@ -0,0 +1,256 @@ |
|||||
|
# 1.2 Go Module, GOPATH 与工作空间 |
||||
|
|
||||
|
从 Go1.11 开始, Go 官方加入 Go Module 支持, Go1.12 成为默认支持; 从此告别源码必须放在 Gopath 中 |
||||
|
以及 Gopath 对初学者造成的困扰. |
||||
|
|
||||
|
## 使用 Go Module |
||||
|
|
||||
|
由于众所周知的原因, 要顺利安装 Go Module 的包, 需要先设置代理: |
||||
|
|
||||
|
公共模块代理: |
||||
|
```sh |
||||
|
export GOPROXY=https://goproxy.io // 也可以设置为 https://goproxy.cn 或者其他 |
||||
|
``` |
||||
|
|
||||
|
私有模块代理: |
||||
|
```sh |
||||
|
export GOPRIVATE=git.xxx.com |
||||
|
``` |
||||
|
|
||||
|
初始化: |
||||
|
``` |
||||
|
go mod init [module 名称] |
||||
|
``` |
||||
|
|
||||
|
检测和清理依赖: |
||||
|
``` |
||||
|
go mod tidy |
||||
|
``` |
||||
|
|
||||
|
安装指定包: |
||||
|
``` |
||||
|
go get -v github/com/go-ego/gse@v0.60.0-rc4.2 |
||||
|
``` |
||||
|
|
||||
|
### 更新依赖 |
||||
|
``` |
||||
|
go get -u |
||||
|
``` |
||||
|
更新指定包依赖: |
||||
|
``` |
||||
|
go get -u github.com/go-ego/gse |
||||
|
``` |
||||
|
指定版本: |
||||
|
``` |
||||
|
go get -u github/com/go-ego/gse@v0.60.0-rc4.2 |
||||
|
``` |
||||
|
|
||||
|
### Replace: |
||||
|
|
||||
|
使用命令行: |
||||
|
``` |
||||
|
go mod edit -replace github.com/go-ego/gse=/path/to/local/gse |
||||
|
go mod edit -replace github.com/go-ego/gse=github.com/vcaesar/gse |
||||
|
``` |
||||
|
也可以直接修改模块文件: |
||||
|
|
||||
|
``` |
||||
|
replace github.com/go-ego/gse => github.com/vcaesar/gse |
||||
|
``` |
||||
|
|
||||
|
## GOMODULE 常用命令: |
||||
|
|
||||
|
``` |
||||
|
go mod init # 初始化 go.mod |
||||
|
go mod tidy # 更新依赖文件 |
||||
|
go mod download # 下载依赖文件 |
||||
|
|
||||
|
go mod vendor # 将依赖转移至本地的 vendor 文件 |
||||
|
go mod edit # 手动修改依赖文件 |
||||
|
go mod graph # 打印依赖图 |
||||
|
go mod verify # 校验依赖 |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
前面我们在安装 Go 的时候看到需要设置 GOPATH 变量,Go 从1.1版本到1.7必须设置这个变量,而且不能和Go的安装目录一样,这个目录用来存放Go源码,Go的可运行文件,以及相应的编译之后的包文件。所以这个目录下面有三个子目录:src、bin、pkg |
||||
|
|
||||
|
从go 1.8开始,GOPATH 环境变量现在有一个默认值,如果它没有被设置。 它在Unix上默认为$HOME/go,在Windows上默认为%USERPROFILE%/go。 |
||||
|
## GOPATH设置 |
||||
|
go 命令依赖一个重要的环境变量:$GOPATH |
||||
|
|
||||
|
Windows系统中环境变量的形式为`%GOPATH%`,本书主要使用Unix形式,Windows用户请自行替换。 |
||||
|
|
||||
|
*(注:这个不是Go安装目录。下面以笔者的工作目录为示例,如果你想不一样请把GOPATH替换成你的工作目录。)* |
||||
|
|
||||
|
在类 Unix 环境下大概这样设置: |
||||
|
```sh |
||||
|
export GOPATH=/home/apple/mygo |
||||
|
``` |
||||
|
|
||||
|
为了方便,应该新建以上文件夹,并且上一行加入到 `.bashrc` 或者 `.zshrc` 或者自己的 `sh` 的配置文件中。 |
||||
|
|
||||
|
Windows 设置如下,新建一个环境变量名称叫做GOPATH: |
||||
|
```sh |
||||
|
GOPATH=c:\mygo |
||||
|
``` |
||||
|
GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个目录的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get的内容放在第一个目录下。 |
||||
|
|
||||
|
|
||||
|
以上 $GOPATH 目录约定有三个子目录: |
||||
|
|
||||
|
- src 存放源代码(比如:.go .c .h .s等) |
||||
|
- pkg 编译后生成的文件(比如:.a) |
||||
|
- bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个gopath,那么使用`${GOPATH//://bin:}/bin`添加所有的bin目录) |
||||
|
|
||||
|
以后我所有的例子都是以mygo作为我的gopath目录 |
||||
|
|
||||
|
|
||||
|
## 代码目录结构规划 |
||||
|
GOPATH下的src目录就是接下来开发程序的主要目录,所有的源码都是放在这个目录下面,那么一般我们的做法就是一个目录一个项目,例如: $GOPATH/src/mymath 表示mymath这个应用包或者可执行应用,这个根据package是main还是其他来决定,main的话就是可执行应用,其他的话就是应用包,这个会在后续详细介绍package。 |
||||
|
|
||||
|
|
||||
|
所以当新建应用或者一个代码包时都是在src目录下新建一个文件夹,文件夹名称一般是代码包名称,当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb 那么这个包路径就是"github.com/astaxie/beedb",包名称是最后一个目录beedb |
||||
|
|
||||
|
下面我就以mymath为例来讲述如何编写应用包,执行如下代码 |
||||
|
```sh |
||||
|
cd $GOPATH/src |
||||
|
mkdir mymath |
||||
|
``` |
||||
|
|
||||
|
新建文件sqrt.go,内容如下 |
||||
|
```go |
||||
|
// $GOPATH/src/mymath/sqrt.go源码如下: |
||||
|
package mymath |
||||
|
|
||||
|
func Sqrt(x float64) float64 { |
||||
|
z := 0.0 |
||||
|
for i := 0; i < 1000; i++ { |
||||
|
z -= (z*z - x) / (2 * x) |
||||
|
} |
||||
|
return z |
||||
|
} |
||||
|
``` |
||||
|
这样我的应用包目录和代码已经新建完毕,注意:一般建议package的名称和目录名保持一致 |
||||
|
|
||||
|
## 编译应用 |
||||
|
上面我们已经建立了自己的应用包,如何进行编译安装呢?有两种方式可以进行安装 |
||||
|
|
||||
|
1、只要进入对应的应用包目录,然后执行`go install`,就可以安装了 |
||||
|
|
||||
|
2、在任意的目录执行如下代码`go install mymath` |
||||
|
|
||||
|
安装完之后,我们可以进入如下目录 |
||||
|
```sh |
||||
|
cd $GOPATH/pkg/${GOOS}_${GOARCH} |
||||
|
//可以看到如下文件 |
||||
|
mymath.a |
||||
|
``` |
||||
|
这个.a文件是应用包,那么我们如何进行调用呢? |
||||
|
|
||||
|
接下来我们新建一个应用程序来调用这个应用包 |
||||
|
|
||||
|
新建应用包mathapp |
||||
|
```sh |
||||
|
cd $GOPATH/src |
||||
|
mkdir mathapp |
||||
|
cd mathapp |
||||
|
vim main.go |
||||
|
``` |
||||
|
|
||||
|
`$GOPATH/src/mathapp/main.go`源码: |
||||
|
```go |
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"mymath" |
||||
|
"fmt" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
可以看到这个的package是`main`,import里面调用的包是`mymath`,这个就是相对于`$GOPATH/src`的路径,如果是多级目录,就在import里面引入多级目录,如果你有多个GOPATH,也是一样,Go会自动在多个`$GOPATH/src`中寻找。 |
||||
|
|
||||
|
如何编译程序呢?进入该应用目录,然后执行`go build`,那么在该目录下面会生成一个mathapp的可执行文件 |
||||
|
```sh |
||||
|
./mathapp |
||||
|
``` |
||||
|
|
||||
|
输出如下内容 |
||||
|
```sh |
||||
|
Hello, world. Sqrt(2) = 1.414213562373095 |
||||
|
``` |
||||
|
|
||||
|
如何安装该应用,进入该目录执行`go install`,那么在$GOPATH/bin/下增加了一个可执行文件mathapp, 还记得前面我们把`$GOPATH/bin`加到我们的PATH里面了,这样可以在命令行输入如下命令就可以执行 |
||||
|
|
||||
|
```sh |
||||
|
mathapp |
||||
|
``` |
||||
|
|
||||
|
也是输出如下内容 |
||||
|
|
||||
|
Hello, world. Sqrt(2) = 1.414213562373095 |
||||
|
|
||||
|
这里我们展示如何编译和安装一个可运行的应用,以及如何设计我们的目录结构。 |
||||
|
|
||||
|
对于启用go module的Go版本,需要对mod.go进行配置。(使用replace的方式将远程包替换为本地包) |
||||
|
|
||||
|
## 获取远程包 |
||||
|
go语言有一个获取远程包的工具就是`go get`,目前go get支持多数开源社区(例如:GitHub、googlecode、bitbucket、Launchpad) |
||||
|
|
||||
|
go get github.com/astaxie/beedb |
||||
|
|
||||
|
>go get -u 参数可以自动更新包,而且当go get的时候会自动获取该包依赖的其他第三方包 |
||||
|
|
||||
|
通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,例如GitHub采用git、googlecode采用hg,所以要想获取这些源码,必须先安装相应的源码控制工具 |
||||
|
|
||||
|
通过上面获取的代码在我们本地的源码相应的代码结构如下 |
||||
|
|
||||
|
$GOPATH |
||||
|
src |
||||
|
|--github.com |
||||
|
|-astaxie |
||||
|
|-beedb |
||||
|
pkg |
||||
|
|--相应平台 |
||||
|
|-github.com |
||||
|
|--astaxie |
||||
|
|beedb.a |
||||
|
|
||||
|
go get本质上可以理解为首先第一步是通过源码工具clone代码到src下面,然后执行`go install` |
||||
|
|
||||
|
在代码中如何使用远程包,很简单的就是和使用本地包一样,只要在开头import相应的路径就可以 |
||||
|
|
||||
|
import "github.com/astaxie/beedb" |
||||
|
|
||||
|
## 程序的整体结构 |
||||
|
通过上面建立的我本地的mygo的目录结构如下所示 |
||||
|
|
||||
|
bin/ |
||||
|
mathapp |
||||
|
pkg/ |
||||
|
平台名/ 如:darwin_amd64、linux_amd64 |
||||
|
mymath.a |
||||
|
github.com/ |
||||
|
astaxie/ |
||||
|
beedb.a |
||||
|
src/ |
||||
|
mathapp |
||||
|
main.go |
||||
|
mymath/ |
||||
|
sqrt.go |
||||
|
github.com/ |
||||
|
astaxie/ |
||||
|
beedb/ |
||||
|
beedb.go |
||||
|
util.go |
||||
|
|
||||
|
从上面的结构我们可以很清晰的看到,bin目录下面存的是编译之后可执行的文件,pkg下面存放的是应用包,src下面保存的是应用源代码 |
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [安装Go](<01.1.md>) |
||||
|
* 下一节: [GO 命令](<01.3.md>) |
@ -0,0 +1,648 @@ |
|||||
|
# 1.4 Go开发工具 |
||||
|
|
||||
|
本节我将介绍几个开发工具,它们都具有自动化补全,自动化 fmt 和检查等功能。因为它们都是跨平台的,所以安装步骤之类的都是通用的。 |
||||
|
|
||||
|
## Visual Studio Code |
||||
|
|
||||
|
Vscode 是微软基于 Electron 和 Web 技术构建的开源编辑器, 是一款非常强大的编辑器。 |
||||
|
而且目前 vscode-go 已升级为 Go 官方维护的工具, 也是当前免费开源的首选开发工具. |
||||
|
|
||||
|
开源地址: https://github.com/Microsoft/vscode |
||||
|
|
||||
|
1、安装Visual Studio Code 最新版 |
||||
|
|
||||
|
官方网站:https://code.visualstudio.com |
||||
|
下载 Visual Studio Code 最新版,安装过程略。 |
||||
|
|
||||
|
2、安装 Go 插件 |
||||
|
|
||||
|
点击右边的 Extensions/扩展 图标 |
||||
|
搜索 `Go` 插件 |
||||
|
在插件列表中,选择 Go,进行安装,安装之后,系统会提示重启 Visual Studio Code。 |
||||
|
|
||||
|
写一个 Hello World ,然后运行: |
||||
|
|
||||
|
hello.go |
||||
|
```Go |
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
func main() { |
||||
|
fmt.Printf("Hello word") |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
保存的时候, vscode 右下角会提示,需要安装依赖包, 点击 Install All ( 建议提前设好代理, 参考 Go Module 章节 ) |
||||
|
另外建议使用 golint 或者 golangci-lint 等工具, 可以写出更优雅的代码; 也可以把自动保存功能开启, 开启方法为:选择菜单 File,点击 Auto save。 |
||||
|
|
||||
|
VSCode 代码设置可用于 Go 扩展。这些都可以在用户的喜好来设置或工作区设置(.vscode/settings.json)。 |
||||
|
|
||||
|
VSCode 还有一些高级配置, 打开首选项-用户设置, 一般可以通过图形界面设置 |
||||
|
也可以通过 settings.json 进行更多高级配置: |
||||
|
|
||||
|
```Go |
||||
|
"go.buildOnSave": "package", // 'workspace', 'package', or 'off'. |
||||
|
"go.lintOnSave": "workspace", // 'workspace', 'package', or 'off'. |
||||
|
"go.vetOnSave": "workspace", // 'workspace', 'package', or 'off'. |
||||
|
"go.buildFlags": [], |
||||
|
"go.lintFlags": [], |
||||
|
"go.vetFlags": [], |
||||
|
"go.coverOnSave": false, |
||||
|
"go.useCodeSnippetsOnFunctionSuggest": false, |
||||
|
"go.formatOnSave": true, |
||||
|
// |
||||
|
"go.formatTool": "goimports", // goreturns |
||||
|
"go.lintTool": "golangci-lint", |
||||
|
"go.useLanguageServer": true, |
||||
|
// |
||||
|
"go.goroot": "", // 你的 Goroot, 可选配置 |
||||
|
"go.gopath": "", // 你的 Gopath, go1.12 之后改用 go-mod, 可选配置 |
||||
|
``` |
||||
|
|
||||
|
vscode 还有一项很强大的功能就是断点调试,结合 [delve](https://github.com/derekparker/delve) 可以很好的进行 Go 代码调试 |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
go get -v -u github.com/peterh/liner github.com/derekparker/delve/cmd/dlv |
||||
|
|
||||
|
brew install go-delve/delve/delve (mac可选) |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
如果有问题再来一遍: |
||||
|
```Go |
||||
|
go get -v -u github.com/peterh/liner github.com/derekparker/delve/cmd/dlv |
||||
|
``` |
||||
|
|
||||
|
注意:修改"dlv-cert"证书, 选择"显示简介"->"信任"->"代码签名" 修改为: 始终信任 |
||||
|
|
||||
|
打开首选项-工作区设置,配置launch.json: |
||||
|
|
||||
|
```Go |
||||
|
{ |
||||
|
"version": "0.2.0", |
||||
|
"configurations": [ |
||||
|
{ |
||||
|
"name": "main.go", |
||||
|
"type": "go", |
||||
|
"request": "launch", |
||||
|
"mode": "debug", |
||||
|
"remotePath": "", |
||||
|
"port": 2345, |
||||
|
"host": "127.0.0.1", |
||||
|
"program": "${workspaceRoot}", // 工作空间路径 |
||||
|
"env": {}, |
||||
|
"args": [], |
||||
|
"showLog": true |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## GoLand |
||||
|
|
||||
|
GoLand 是 JetBrains 公司推出的 Go 语言集成开发环境,是 Idea Go 插件的强化版。GoLand 同样基于 IntelliJ 平台开发,支持 JetBrains 的插件体系。 |
||||
|
经过几年不断迭代更新, GoLand 目前已经是 Go 开发工具中最成熟的 IDE 之一, 而且配置相当简单. |
||||
|
|
||||
|
下载地址: https://www.jetbrains.com/go/ |
||||
|
|
||||
|
|
||||
|
## Sublime Text |
||||
|
|
||||
|
这里将介绍Sublime Text 3(以下简称Sublime)+ GoSublime + gocode的组合,那么为什么选择这个组合呢? |
||||
|
|
||||
|
- 自动化提示代码,如下图所示 |
||||
|
|
||||
|
![](images/1.4.sublime1.png?raw=true) |
||||
|
|
||||
|
图1.5 sublime自动化提示界面 |
||||
|
|
||||
|
- 保存的时候自动格式化代码,让您编写的代码更加美观,符合Go的标准。 |
||||
|
- 支持项目管理 |
||||
|
|
||||
|
![](images/1.4.sublime2.png?raw=true) |
||||
|
|
||||
|
图1.6 sublime项目管理界面 |
||||
|
|
||||
|
- 支持语法高亮 |
||||
|
- Sublime Text 3可免费使用,只是保存次数达到一定数量之后就会提示是否购买,点击取消继续用,和正式注册版本没有任何区别。 |
||||
|
|
||||
|
|
||||
|
接下来就开始讲如何安装,下载 [Sublime](http://www.sublimetext.com/) |
||||
|
|
||||
|
根据自己相应的系统下载相应的版本,然后打开Sublime,对于不熟悉Sublime的同学可以先看一下这篇文章[Sublime Text 全程指南](http://blog.jobbole.com/88648/)或者[sublime text3入门教程](http://blog.csdn.net/sam976/article/details/52076271) |
||||
|
|
||||
|
1. 打开之后安装 Package Control:Ctrl+` 打开命令行,执行如下代码: |
||||
|
|
||||
|
适用于 Sublime Text 3: |
||||
|
|
||||
|
```Go |
||||
|
import urllib.request,os;pf='Package Control.sublime-package';ipp=sublime.installed_packages_path();urllib.request.install_opener(urllib.request.build_opener(urllib.request.ProxyHandler()));open(os.path.join(ipp,pf),'wb').write(urllib.request.urlopen('http://sublime.wbond.net/'+pf.replace(' ','%20')).read()) |
||||
|
``` |
||||
|
适用于 Sublime Text 2: |
||||
|
|
||||
|
```Go |
||||
|
import urllib2,os;pf='Package Control.sublime-package';ipp=sublime.installed_packages_path();os.makedirs(ipp)ifnotos.path.exists(ipp)elseNone;urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler()));open(os.path.join(ipp,pf),'wb').write(urllib2.urlopen('http://sublime.wbond.net/'+pf.replace(' ','%20')).read());print('Please restart Sublime Text to finish installation') |
||||
|
``` |
||||
|
|
||||
|
这个时候重启一下Sublime,可以发现在在菜单栏多了一个如下的栏目,说明Package Control已经安装成功了。 |
||||
|
|
||||
|
![](images/1.4.sublime3.png?raw=true) |
||||
|
|
||||
|
图1.7 sublime包管理 |
||||
|
|
||||
|
|
||||
|
2. 安装完之后就可以安装Sublime的插件了。需安装GoSublime、SidebarEnhancements和Go Build,安装插件之后记得重启Sublime生效,Ctrl+Shift+p打开Package Controll 输入`pcip`(即“Package Control: Install Package”的缩写)。 |
||||
|
|
||||
|
这个时候看左下角显示正在读取包数据,完成之后出现如下界面 |
||||
|
|
||||
|
![](images/1.4.sublime4.png?raw=true) |
||||
|
|
||||
|
图1.8 sublime安装插件界面 |
||||
|
|
||||
|
这个时候输入GoSublime,按确定就开始安装了。同理应用于SidebarEnhancements和Go Build。 |
||||
|
|
||||
|
3. 安装 [gocode](https://github.com/nsf/gocode/) |
||||
|
|
||||
|
go get -u github.com/nsf/gocode |
||||
|
|
||||
|
gocode 将会安装在默认`$GOBIN` |
||||
|
|
||||
|
另外建议安装gotests(生成测试代码): |
||||
|
|
||||
|
|
||||
|
先在sublime安装gotests插件,再运行: |
||||
|
|
||||
|
|
||||
|
```Go |
||||
|
go get -u -v github.com/cweill/gotests/... |
||||
|
``` |
||||
|
|
||||
|
3. 验证是否安装成功,你可以打开Sublime,打开main.go,看看语法是不是高亮了,输入`import`是不是自动化提示了,`import "fmt"`之后,输入`fmt.`是不是自动化提示有函数了。 |
||||
|
|
||||
|
如果已经出现这个提示,那说明你已经安装完成了,并且完成了自动提示。 |
||||
|
|
||||
|
如果没有出现这样的提示,一般就是你的`$PATH`没有配置正确。你可以打开终端,输入gocode,是不是能够正确运行,如果不行就说明`$PATH`没有配置正确。 |
||||
|
(针对XP)有时候在终端能运行成功,但sublime无提示或者编译解码错误,请安装sublime text3和convert utf8插件试一试 |
||||
|
|
||||
|
4. MacOS下已经设置了$GOROOT, $GOPATH, $GOBIN,还是没有自动提示怎么办。 |
||||
|
|
||||
|
请在sublime中使用command + 9, 然后输入env检查$PATH, GOROOT, $GOPATH, $GOBIN等变量, 如果没有请采用下面的方法。 |
||||
|
|
||||
|
首先建立下面的连接, 然后从Terminal中直接启动sublime |
||||
|
|
||||
|
ln -s /Applications/Sublime\ Text\ 2.app/Contents/SharedSupport/bin/subl /usr/local/bin/sublime |
||||
|
|
||||
|
|
||||
|
## LiteIDE |
||||
|
|
||||
|
LiteIDE是一款专门为Go语言开发的跨平台轻量级集成开发环境(IDE),由visualfc编写。 |
||||
|
|
||||
|
![](images/1.4.liteide.png?raw=true) |
||||
|
|
||||
|
图1.4 LiteIDE主界面 |
||||
|
|
||||
|
**LiteIDE主要特点:** |
||||
|
|
||||
|
* 支持主流操作系统 |
||||
|
* Windows |
||||
|
* Linux |
||||
|
* MacOS X |
||||
|
* Go编译环境管理和切换 |
||||
|
* 管理和切换多个Go编译环境 |
||||
|
* 支持Go语言交叉编译 |
||||
|
* 与Go标准一致的项目管理方式 |
||||
|
* 基于GOPATH的包浏览器 |
||||
|
* 基于GOPATH的编译系统 |
||||
|
* 基于GOPATH的Api文档检索 |
||||
|
* Go语言的编辑支持 |
||||
|
* 类浏览器和大纲显示 |
||||
|
* Gocode(代码自动完成工具)的完美支持 |
||||
|
* Go语言文档查看和Api快速检索 |
||||
|
* 代码表达式信息显示`F1` |
||||
|
* 源代码定义跳转支持`F2` |
||||
|
* Gdb断点和调试支持 |
||||
|
* gofmt自动格式化支持 |
||||
|
* 其他特征 |
||||
|
* 支持多国语言界面显示 |
||||
|
* 完全插件体系结构 |
||||
|
* 支持编辑器配色方案 |
||||
|
* 基于Kate的语法显示支持 |
||||
|
* 基于全文的单词自动完成 |
||||
|
* 支持键盘快捷键绑定方案 |
||||
|
* Markdown文档编辑支持 |
||||
|
* 实时预览和同步显示 |
||||
|
* 自定义CSS显示 |
||||
|
* 可导出HTML和PDF文档 |
||||
|
* 批量转换/合并为HTML/PDF文档 |
||||
|
|
||||
|
**LiteIDE安装配置** |
||||
|
|
||||
|
* LiteIDE安装 |
||||
|
* 下载地址 <http://sourceforge.net/projects/liteide/files> |
||||
|
* 源码地址 <https://github.com/visualfc/liteide> |
||||
|
|
||||
|
首先安装好Go语言环境,然后根据操作系统下载LiteIDE对应的压缩文件直接解压即可使用。 |
||||
|
|
||||
|
* 编译环境设置 |
||||
|
|
||||
|
根据自身系统要求切换和配置LiteIDE当前使用的环境变量。 |
||||
|
|
||||
|
以Windows操作系统,64位Go语言为例, |
||||
|
工具栏的环境配置中选择win64,点`编辑环境`,进入LiteIDE编辑win64.env文件 |
||||
|
|
||||
|
GOROOT=c:\go |
||||
|
GOBIN= |
||||
|
GOARCH=amd64 |
||||
|
GOOS=windows |
||||
|
CGO_ENABLED=1 |
||||
|
|
||||
|
PATH=%GOBIN%;%GOROOT%\bin;%PATH% |
||||
|
。。。 |
||||
|
|
||||
|
将其中的`GOROOT=c:\go`修改为当前Go安装路径,存盘即可,如果有MinGW64,可以将`c:\MinGW64\bin`加入PATH中以便go调用gcc支持CGO编译。 |
||||
|
|
||||
|
以Linux操作系统,64位Go语言为例, |
||||
|
工具栏的环境配置中选择linux64,点`编辑环境`,进入LiteIDE编辑linux64.env文件 |
||||
|
|
||||
|
GOROOT=$HOME/go |
||||
|
GOBIN= |
||||
|
GOARCH=amd64 |
||||
|
GOOS=linux |
||||
|
CGO_ENABLED=1 |
||||
|
|
||||
|
PATH=$GOBIN:$GOROOT/bin:$PATH |
||||
|
。。。 |
||||
|
|
||||
|
将其中的`GOROOT=$HOME/go`修改为当前Go安装路径,存盘即可。 |
||||
|
|
||||
|
* GOPATH设置 |
||||
|
|
||||
|
Go语言的工具链使用GOPATH设置,是Go语言开发的项目路径列表,在命令行中输入(在LiteIDE中也可以`Ctrl+,`直接输入)`go help gopath`快速查看GOPATH文档。 |
||||
|
|
||||
|
在LiteIDE中可以方便的查看和设置GOPATH。通过`菜单-查看-GOPATH`设置,可以查看系统中已存在的GOPATH列表, |
||||
|
同时可根据需要添加项目目录到自定义GOPATH列表中。 |
||||
|
|
||||
|
|
||||
|
## Atom |
||||
|
|
||||
|
Atom 是 GitHub 基于 Electron 和 Web 技术构建的开源编辑器, 是一款很漂亮强大的编辑器, 缺点是速度比较慢。 |
||||
|
|
||||
|
首先要先安装下 Atom,下载地址: https://atom.io/ |
||||
|
|
||||
|
然后安装 go-plus 插件: |
||||
|
|
||||
|
go-plus 是 Atom 上面的一款开源的 go 语言开发环境的的插件 |
||||
|
|
||||
|
它需要依赖下面的go语言工具: |
||||
|
```Go |
||||
|
1.autocomplete-go :gocode的代码自动提示 |
||||
|
2.gofmt :使用goftm,goimports,goturns |
||||
|
3.builder-go:go-install 和go-test,验证代码,给出建议 |
||||
|
4.gometalinet-linter:goline,vet,gotype的检查 |
||||
|
5.navigator-godef:godef |
||||
|
6.tester-goo :go test |
||||
|
7.gorename :rename |
||||
|
|
||||
|
``` |
||||
|
在Atom中的 Preference 中可以找到install菜单,输入 go-plus,然后点击安装(install) |
||||
|
|
||||
|
就会开始安装 go-plus , go-plus 插件会自动安装对应的依赖插件,如果没有安装对应的go的类库会自动运行: go get 安装。 |
||||
|
|
||||
|
|
||||
|
## Vim |
||||
|
|
||||
|
Vim是从vi发展出来的一个文本编辑器, 代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。 |
||||
|
|
||||
|
vim-go是vim上面的一款开源的go语言使用最为广泛开发环境的的插件 |
||||
|
|
||||
|
插件地址:[github.com/fatih/vim-go](https://github.com/fatih/vim-go) |
||||
|
|
||||
|
vim的插件管理主要有[Pathogen](https://github.com/tpope/vim-pathogen)与[Vundle](https://github.com/VundleVim/Vundle.vim) |
||||
|
,但是其作用的方面不同。 |
||||
|
pathogen是为了解决每一个插件安装后文件分散到多个目录不好管理而存在的。vundle是为了解决自动搜索及下载插件而存在的。 |
||||
|
这两个插件可同时使用。 |
||||
|
|
||||
|
1.安装 Vundle |
||||
|
|
||||
|
```sh |
||||
|
mkdir ~/.vim/bundle |
||||
|
git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim |
||||
|
``` |
||||
|
|
||||
|
修改.vimrc,将Vundle的相关配置置在最开始处([详细参考Vundle的介绍文档](https://github.com/VundleVim/Vundle.vim)) |
||||
|
|
||||
|
```sh |
||||
|
set nocompatible " be iMproved, required |
||||
|
filetype off " required |
||||
|
|
||||
|
" set the runtime path to include Vundle and initialize |
||||
|
set rtp+=~/.vim/bundle/Vundle.vim |
||||
|
call vundle#begin() |
||||
|
|
||||
|
" let Vundle manage Vundle, required |
||||
|
Plugin 'gmarik/Vundle.vim' |
||||
|
|
||||
|
" All of your Plugins must be added before the following line |
||||
|
call vundle#end() " required |
||||
|
filetype plugin indent on " required |
||||
|
|
||||
|
``` |
||||
|
2.安装Vim-go |
||||
|
|
||||
|
修改~/.vimrc,在vundle#begin和vundle#end间增加一行: |
||||
|
|
||||
|
```sh |
||||
|
|
||||
|
Plugin 'fatih/vim-go' |
||||
|
``` |
||||
|
|
||||
|
在Vim内执行: PluginInstall |
||||
|
|
||||
|
3.安装YCM(Your Complete Me)进行自动补全 |
||||
|
在~/.vimrc中添加一行: |
||||
|
```sh |
||||
|
|
||||
|
Plugin 'Valloric/YouCompleteMe' |
||||
|
``` |
||||
|
在Vim内执行: PluginInstall |
||||
|
|
||||
|
|
||||
|
![](images/1.4.vim.png?raw=true) |
||||
|
|
||||
|
图1.9 VIM编辑器自动化提示Go界面 |
||||
|
|
||||
|
接着我们继续配置vim: |
||||
|
|
||||
|
1. 配置vim高亮显示 |
||||
|
|
||||
|
cp -r $GOROOT/misc/vim/* ~/.vim/ |
||||
|
|
||||
|
2. 在~/.vimrc文件中增加语法高亮显示 |
||||
|
|
||||
|
filetype plugin indent on |
||||
|
syntax on |
||||
|
|
||||
|
3. 安装[Gocode](https://github.com/nsf/gocode/) |
||||
|
|
||||
|
go get -u github.com/nsf/gocode |
||||
|
|
||||
|
gocode默认安装到`$GOPATH/bin`下面。 |
||||
|
|
||||
|
4. 配置[Gocode](https://github.com/nsf/gocode/) |
||||
|
|
||||
|
~ cd $GOPATH/src/github.com/nsf/gocode/vim |
||||
|
~ ./update.bash |
||||
|
~ gocode set propose-builtins true |
||||
|
propose-builtins true |
||||
|
~ gocode set lib-path "/home/border/gocode/pkg/linux_amd64" |
||||
|
lib-path "/home/border/gocode/pkg/linux_amd64" |
||||
|
~ gocode set |
||||
|
propose-builtins true |
||||
|
lib-path "/home/border/gocode/pkg/linux_amd64" |
||||
|
|
||||
|
>gocode set里面的两个参数的含意说明: |
||||
|
> |
||||
|
>propose-builtins:是否自动提示Go的内置函数、类型和常量,默认为false,不提示。 |
||||
|
> |
||||
|
>lib-path:默认情况下,gocode只会搜索**$GOPATH/pkg/$GOOS_$GOARCH** 和 **$GOROOT/pkg/$GOOS_$GOARCH**目录下的包,当然这个设置就是可以设置我们额外的lib能访问的路径 |
||||
|
|
||||
|
|
||||
|
5. 恭喜你,安装完成,你现在可以使用`:e main.go`体验一下开发Go的乐趣。 |
||||
|
|
||||
|
更多VIM 设定, 可参考[链接](http://www.cnblogs.com/witcxc/archive/2011/12/28/2304704.html) |
||||
|
|
||||
|
## Emacs |
||||
|
|
||||
|
Emacs传说中的神器,她不仅仅是一个编辑器,它是一个整合环境,或可称它为集成开发环境,这些功能如让使用者置身于全功能的操作系统中。 |
||||
|
|
||||
|
![](images/1.4.emacs.png?raw=true) |
||||
|
|
||||
|
图1.10 Emacs编辑Go主界面 |
||||
|
|
||||
|
1. 配置Emacs高亮显示 |
||||
|
|
||||
|
cp $GOROOT/misc/emacs/* ~/.emacs.d/ |
||||
|
|
||||
|
2. 安装[Gocode](https://github.com/nsf/gocode/) |
||||
|
|
||||
|
go get -u github.com/nsf/gocode |
||||
|
|
||||
|
gocode默认安装到`$GOBIN`里面下面。 |
||||
|
|
||||
|
3. 配置[Gocode](https://github.com/nsf/gocode/) |
||||
|
|
||||
|
|
||||
|
~ cd $GOPATH/src/github.com/nsf/gocode/emacs |
||||
|
~ cp go-autocomplete.el ~/.emacs.d/ |
||||
|
~ gocode set propose-builtins true |
||||
|
propose-builtins true |
||||
|
~ gocode set lib-path "/home/border/gocode/pkg/linux_amd64" // 换为你自己的路径 |
||||
|
lib-path "/home/border/gocode/pkg/linux_amd64" |
||||
|
~ gocode set |
||||
|
propose-builtins true |
||||
|
lib-path "/home/border/gocode/pkg/linux_amd64" |
||||
|
|
||||
|
4. 需要安装 [Auto Completion](http://www.emacswiki.org/emacs/AutoComplete) |
||||
|
|
||||
|
下载AutoComplete并解压 |
||||
|
|
||||
|
~ make install DIR=$HOME/.emacs.d/auto-complete |
||||
|
|
||||
|
配置~/.emacs文件 |
||||
|
|
||||
|
;;auto-complete |
||||
|
(require 'auto-complete-config) |
||||
|
(add-to-list 'ac-dictionary-directories "~/.emacs.d/auto-complete/ac-dict") |
||||
|
(ac-config-default) |
||||
|
(local-set-key (kbd "M-/") 'semantic-complete-analyze-inline) |
||||
|
(local-set-key "." 'semantic-complete-self-insert) |
||||
|
(local-set-key ">" 'semantic-complete-self-insert) |
||||
|
|
||||
|
详细信息参考: http://www.emacswiki.org/emacs/AutoComplete |
||||
|
|
||||
|
5. 配置.emacs |
||||
|
|
||||
|
;; golang mode |
||||
|
(require 'go-mode-load) |
||||
|
(require 'go-autocomplete) |
||||
|
;; speedbar |
||||
|
;; (speedbar 1) |
||||
|
(speedbar-add-supported-extension ".go") |
||||
|
(add-hook |
||||
|
'go-mode-hook |
||||
|
'(lambda () |
||||
|
;; gocode |
||||
|
(auto-complete-mode 1) |
||||
|
(setq ac-sources '(ac-source-go)) |
||||
|
;; Imenu & Speedbar |
||||
|
(setq imenu-generic-expression |
||||
|
'(("type" "^type *\\([^ \t\n\r\f]*\\)" 1) |
||||
|
("func" "^func *\\(.*\\) {" 1))) |
||||
|
(imenu-add-to-menubar "Index") |
||||
|
;; Outline mode |
||||
|
(make-local-variable 'outline-regexp) |
||||
|
(setq outline-regexp "//\\.\\|//[^\r\n\f][^\r\n\f]\\|pack\\|func\\|impo\\|cons\\|var.\\|type\\|\t\t*....") |
||||
|
(outline-minor-mode 1) |
||||
|
(local-set-key "\M-a" 'outline-previous-visible-heading) |
||||
|
(local-set-key "\M-e" 'outline-next-visible-heading) |
||||
|
;; Menu bar |
||||
|
(require 'easymenu) |
||||
|
(defconst go-hooked-menu |
||||
|
'("Go tools" |
||||
|
["Go run buffer" go t] |
||||
|
["Go reformat buffer" go-fmt-buffer t] |
||||
|
["Go check buffer" go-fix-buffer t])) |
||||
|
(easy-menu-define |
||||
|
go-added-menu |
||||
|
(current-local-map) |
||||
|
"Go tools" |
||||
|
go-hooked-menu) |
||||
|
|
||||
|
;; Other |
||||
|
(setq show-trailing-whitespace t) |
||||
|
)) |
||||
|
;; helper function |
||||
|
(defun go () |
||||
|
"run current buffer" |
||||
|
(interactive) |
||||
|
(compile (concat "go run " (buffer-file-name)))) |
||||
|
|
||||
|
;; helper function |
||||
|
(defun go-fmt-buffer () |
||||
|
"run gofmt on current buffer" |
||||
|
(interactive) |
||||
|
(if buffer-read-only |
||||
|
(progn |
||||
|
(ding) |
||||
|
(message "Buffer is read only")) |
||||
|
(let ((p (line-number-at-pos)) |
||||
|
(filename (buffer-file-name)) |
||||
|
(old-max-mini-window-height max-mini-window-height)) |
||||
|
(show-all) |
||||
|
(if (get-buffer "*Go Reformat Errors*") |
||||
|
(progn |
||||
|
(delete-windows-on "*Go Reformat Errors*") |
||||
|
(kill-buffer "*Go Reformat Errors*"))) |
||||
|
(setq max-mini-window-height 1) |
||||
|
(if (= 0 (shell-command-on-region (point-min) (point-max) "gofmt" "*Go Reformat Output*" nil "*Go Reformat Errors*" t)) |
||||
|
(progn |
||||
|
(erase-buffer) |
||||
|
(insert-buffer-substring "*Go Reformat Output*") |
||||
|
(goto-char (point-min)) |
||||
|
(forward-line (1- p))) |
||||
|
(with-current-buffer "*Go Reformat Errors*" |
||||
|
(progn |
||||
|
(goto-char (point-min)) |
||||
|
(while (re-search-forward "<standard input>" nil t) |
||||
|
(replace-match filename)) |
||||
|
(goto-char (point-min)) |
||||
|
(compilation-mode)))) |
||||
|
(setq max-mini-window-height old-max-mini-window-height) |
||||
|
(delete-windows-on "*Go Reformat Output*") |
||||
|
(kill-buffer "*Go Reformat Output*")))) |
||||
|
;; helper function |
||||
|
(defun go-fix-buffer () |
||||
|
"run gofix on current buffer" |
||||
|
(interactive) |
||||
|
(show-all) |
||||
|
(shell-command-on-region (point-min) (point-max) "go tool fix -diff")) |
||||
|
|
||||
|
6. 恭喜你,你现在可以体验在神器中开发Go的乐趣。默认speedbar是关闭的,如果打开需要把 ;; (speedbar 1) 前面的注释去掉,或者也可以通过 *M-x speedbar* 手动开启。 |
||||
|
|
||||
|
## Eclipse |
||||
|
|
||||
|
Eclipse也是非常常用的开发利器,以下介绍如何使用Eclipse来编写Go程序。 |
||||
|
|
||||
|
![](images/1.4.eclipse1.png?raw=true) |
||||
|
|
||||
|
图1.11 Eclipse编辑Go的主界面 |
||||
|
|
||||
|
1. 首先下载并安装好[Eclipse](http://www.eclipse.org/) |
||||
|
|
||||
|
2. 下载[goclipse](https://code.google.com/p/goclipse/)插件 |
||||
|
|
||||
|
http://code.google.com/p/goclipse/wiki/InstallationInstructions |
||||
|
|
||||
|
3. 下载gocode,用于go的代码补全提示 |
||||
|
|
||||
|
gocode的GitHub地址: |
||||
|
|
||||
|
https://github.com/nsf/gocode |
||||
|
|
||||
|
在windows下要安装git,通常用[msysgit](https://code.google.com/p/msysgit/) |
||||
|
|
||||
|
再在cmd下安装: |
||||
|
|
||||
|
go get -u github.com/nsf/gocode |
||||
|
|
||||
|
也可以下载代码,直接用go build来编译,会生成gocode.exe |
||||
|
|
||||
|
4. 下载[MinGW](http://sourceforge.net/projects/mingw/files/MinGW/)并按要求装好 |
||||
|
|
||||
|
5. 配置插件 |
||||
|
|
||||
|
Windows->Reference->Go |
||||
|
|
||||
|
(1).配置Go的编译器 |
||||
|
|
||||
|
![](images/1.4.eclipse2.png?raw=true) |
||||
|
|
||||
|
图1.12 设置Go的一些基础信息 |
||||
|
|
||||
|
|
||||
|
(2).配置Gocode(可选,代码补全),设置Gocode路径为之前生成的gocode.exe文件 |
||||
|
|
||||
|
![](images/1.4.eclipse3.png?raw=true) |
||||
|
|
||||
|
图1.13 设置gocode信息 |
||||
|
|
||||
|
(3).配置GDB(可选,做调试用),设置GDB路径为MingW安装目录下的gdb.exe文件 |
||||
|
|
||||
|
![](images/1.4.eclipse4.png?raw=true) |
||||
|
|
||||
|
图1.14 设置GDB信息 |
||||
|
|
||||
|
6. 测试是否成功 |
||||
|
|
||||
|
新建一个go工程,再建立一个hello.go。如下图: |
||||
|
|
||||
|
![](images/1.4.eclipse5.png?raw=true) |
||||
|
|
||||
|
图1.15 新建项目编辑文件 |
||||
|
|
||||
|
调试如下(要在console中用输入命令来调试): |
||||
|
|
||||
|
![](images/1.4.eclipse6.png?raw=true) |
||||
|
|
||||
|
图1.16 调试Go程序 |
||||
|
|
||||
|
## IntelliJ IDEA |
||||
|
|
||||
|
熟悉Java的读者应该对于idea不陌生,idea是通过一个插件来支持go语言的高亮语法,代码提示和重构实现。 |
||||
|
|
||||
|
1. 先下载idea,idea支持多平台:win,mac,linux,如果有钱就买个正式版,如果不行就使用社区免费版,对于只是开发Go语言来说免费版足够用了 |
||||
|
|
||||
|
![](images/1.4.idea1.png?raw=true) |
||||
|
|
||||
|
2. 安装Go插件,点击菜单File中的Setting,找到Plugins,点击,Broswer repo按钮。国内的用户可能会报错,自己解决哈。 |
||||
|
|
||||
|
![](images/1.4.idea3.png?raw=true) |
||||
|
|
||||
|
3. 这时候会看见很多插件,搜索找到Golang,双击,download and install。等到golang那一行后面出现Downloaded标志后,点OK。 |
||||
|
|
||||
|
![](images/1.4.idea4.png?raw=true) |
||||
|
|
||||
|
然后点 Apply .这时候IDE会要求你重启。 |
||||
|
|
||||
|
4. 重启完毕后,创建新项目会发现已经可以创建golang项目了: |
||||
|
|
||||
|
![](images/1.4.idea5.png?raw=true) |
||||
|
|
||||
|
下一步,会要求你输入 go sdk的位置,一般都安装在C:\Go,linux和mac根据自己的安装目录设置,选中目录确定,就可以了。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [Go 命令](<01.3.md>) |
||||
|
* 下一节: [总结](<01.5.md>) |
@ -0,0 +1,539 @@ |
|||||
|
# 2.2 Go基础 |
||||
|
|
||||
|
这小节我们将要介绍如何定义变量、常量、Go内置类型以及Go程序设计中的一些技巧。 |
||||
|
|
||||
|
## 定义变量 |
||||
|
|
||||
|
Go语言里面定义变量有多种方式。 |
||||
|
|
||||
|
使用`var`关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面: |
||||
|
```Go |
||||
|
|
||||
|
//定义一个名称为“variableName”,类型为"type"的变量 |
||||
|
var variableName type |
||||
|
``` |
||||
|
定义多个变量 |
||||
|
```Go |
||||
|
|
||||
|
//定义三个类型都是“type”的变量 |
||||
|
var vname1, vname2, vname3 type |
||||
|
``` |
||||
|
定义变量并初始化值 |
||||
|
```Go |
||||
|
|
||||
|
//初始化“variableName”的变量为“value”值,类型是“type” |
||||
|
var variableName type = value |
||||
|
``` |
||||
|
同时初始化多个变量 |
||||
|
```Go |
||||
|
|
||||
|
/* |
||||
|
定义三个类型都是"type"的变量,并且分别初始化为相应的值 |
||||
|
vname1为v1,vname2为v2,vname3为v3 |
||||
|
*/ |
||||
|
var vname1, vname2, vname3 type= v1, v2, v3 |
||||
|
``` |
||||
|
你是不是觉得上面这样的定义有点繁琐?没关系,因为Go语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了: |
||||
|
```Go |
||||
|
|
||||
|
/* |
||||
|
定义三个变量,它们分别初始化为相应的值 |
||||
|
vname1为v1,vname2为v2,vname3为v3 |
||||
|
然后Go会根据其相应值的类型来帮你初始化它们 |
||||
|
*/ |
||||
|
var vname1, vname2, vname3 = v1, v2, v3 |
||||
|
``` |
||||
|
你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化: |
||||
|
```Go |
||||
|
|
||||
|
/* |
||||
|
定义三个变量,它们分别初始化为相应的值 |
||||
|
vname1为v1,vname2为v2,vname3为v3 |
||||
|
编译器会根据初始化的值自动推导出相应的类型 |
||||
|
*/ |
||||
|
vname1, vname2, vname3 := v1, v2, v3 |
||||
|
``` |
||||
|
现在是不是看上去非常简洁了?`:=`这个符号直接取代了`var`和`type`,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用`var`方式来定义全局变量。 |
||||
|
|
||||
|
`_`(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值`35`赋予`b`,并同时丢弃`34`: |
||||
|
|
||||
|
_, b := 34, 35 |
||||
|
|
||||
|
Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了`i`但未使用。 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
func main() { |
||||
|
var i int |
||||
|
} |
||||
|
``` |
||||
|
## 常量 |
||||
|
|
||||
|
所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。 |
||||
|
|
||||
|
它的语法如下: |
||||
|
```Go |
||||
|
|
||||
|
const constantName = value |
||||
|
//如果需要,也可以明确指定常量的类型: |
||||
|
const Pi float32 = 3.1415926 |
||||
|
``` |
||||
|
下面是一些常量声明的例子: |
||||
|
```Go |
||||
|
|
||||
|
const Pi = 3.1415926 |
||||
|
const i = 10000 |
||||
|
const MaxThread = 10 |
||||
|
const prefix = "astaxie_" |
||||
|
``` |
||||
|
Go 常量和一般程序语言不同的是,可以指定相当多的小数位数(例如200位), |
||||
|
若指定給float32自动缩短为32bit,指定给float64自动缩短为64bit,详情参考[链接](http://golang.org/ref/spec#Constants) |
||||
|
|
||||
|
## 内置基础类型 |
||||
|
|
||||
|
### Boolean |
||||
|
|
||||
|
在Go中,布尔值的类型为`bool`,值是`true`或`false`,默认为`false`。 |
||||
|
```Go |
||||
|
|
||||
|
//示例代码 |
||||
|
var isActive bool // 全局变量声明 |
||||
|
var enabled, disabled = true, false // 忽略类型的声明 |
||||
|
func test() { |
||||
|
var available bool // 一般声明 |
||||
|
valid := false // 简短声明 |
||||
|
available = true // 赋值操作 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 数值类型 |
||||
|
|
||||
|
整数类型有无符号和带符号两种。Go同时支持`int`和`uint`,这两种类型的长度相同,但具体长度取决于不同编译器的实现。Go里面也有直接定义好位数的类型:`rune`, `int8`, `int16`, `int32`, `int64`和`byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中`rune`是`int32`的别称,`byte`是`uint8`的别称。 |
||||
|
|
||||
|
>需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。 |
||||
|
> |
||||
|
>如下的代码会产生错误:invalid operation: a + b (mismatched types int8 and int32) |
||||
|
> |
||||
|
>> var a int8 |
||||
|
|
||||
|
>> var b int32 |
||||
|
|
||||
|
>> c:=a + b |
||||
|
> |
||||
|
>另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。 |
||||
|
|
||||
|
浮点数的类型有`float32`和`float64`两种(没有`float`类型),默认是`float64`。 |
||||
|
|
||||
|
这就是全部吗?No!Go还支持复数。它的默认类型是`complex128`(64位实数+64位虚数)。如果需要小一些的,也有`complex64`(32位实数+32位虚数)。复数的形式为`RE + IMi`,其中`RE`是实数部分,`IM`是虚数部分,而最后的`i`是虚数单位。下面是一个使用复数的例子: |
||||
|
```Go |
||||
|
|
||||
|
var c complex64 = 5+5i |
||||
|
//output: (5+5i) |
||||
|
fmt.Printf("Value is: %v", c) |
||||
|
``` |
||||
|
|
||||
|
### 字符串 |
||||
|
|
||||
|
我们在上一节中讲过,Go中的字符串都是采用`UTF-8`字符集编码。字符串是用一对双引号(`""`)或反引号(`` ` `` `` ` ``)括起来定义,它的类型是`string`。 |
||||
|
```Go |
||||
|
|
||||
|
//示例代码 |
||||
|
var frenchHello string // 声明变量为字符串的一般方法 |
||||
|
var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串 |
||||
|
func test() { |
||||
|
no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量 |
||||
|
japaneseHello := "Konichiwa" // 同上 |
||||
|
frenchHello = "Bonjour" // 常规赋值 |
||||
|
} |
||||
|
``` |
||||
|
在Go中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0] |
||||
|
```Go |
||||
|
|
||||
|
var s string = "hello" |
||||
|
s[0] = 'c' |
||||
|
|
||||
|
``` |
||||
|
但如果真的想要修改怎么办呢?下面的代码可以实现: |
||||
|
```Go |
||||
|
|
||||
|
s := "hello" |
||||
|
c := []byte(s) // 将字符串 s 转换为 []byte 类型 |
||||
|
c[0] = 'c' |
||||
|
s2 := string(c) // 再转换回 string 类型 |
||||
|
fmt.Printf("%s\n", s2) |
||||
|
``` |
||||
|
|
||||
|
Go中可以使用`+`操作符来连接两个字符串: |
||||
|
```Go |
||||
|
|
||||
|
s := "hello," |
||||
|
m := " world" |
||||
|
a := s + m |
||||
|
fmt.Printf("%s\n", a) |
||||
|
``` |
||||
|
修改字符串也可写为: |
||||
|
```Go |
||||
|
|
||||
|
s := "hello" |
||||
|
s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作 |
||||
|
fmt.Printf("%s\n", s) |
||||
|
``` |
||||
|
如果要声明一个多行的字符串怎么办?可以通过`` ` ``来声明: |
||||
|
|
||||
|
m := `hello |
||||
|
world` |
||||
|
|
||||
|
`` ` `` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。例如本例中会输出: |
||||
|
|
||||
|
hello |
||||
|
world |
||||
|
|
||||
|
### 错误类型 |
||||
|
Go内置有一个`error`类型,专门用来处理错误信息,Go的`package`里面还专门有一个包`errors`来处理错误: |
||||
|
```Go |
||||
|
|
||||
|
err := errors.New("emit macho dwarf: elf header corrupted") |
||||
|
if err != nil { |
||||
|
fmt.Print(err) |
||||
|
} |
||||
|
``` |
||||
|
### Go数据底层的存储 |
||||
|
|
||||
|
下面这张图来源于[Russ Cox Blog](http://research.swtch.com/)中一篇介绍[Go数据结构](http://research.swtch.com/godata)的文章,大家可以看到这些基础类型底层都是分配了一块内存,然后存储了相应的值。 |
||||
|
|
||||
|
![](images/2.2.basic.png?raw=true) |
||||
|
|
||||
|
图2.1 Go数据格式的存储 |
||||
|
|
||||
|
## 一些技巧 |
||||
|
|
||||
|
### 分组声明 |
||||
|
|
||||
|
在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。 |
||||
|
|
||||
|
例如下面的代码: |
||||
|
```Go |
||||
|
|
||||
|
import "fmt" |
||||
|
import "os" |
||||
|
|
||||
|
const i = 100 |
||||
|
const pi = 3.1415 |
||||
|
const prefix = "Go_" |
||||
|
|
||||
|
var i int |
||||
|
var pi float32 |
||||
|
var prefix string |
||||
|
``` |
||||
|
可以分组写成如下形式: |
||||
|
```Go |
||||
|
|
||||
|
import( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
const( |
||||
|
i = 100 |
||||
|
pi = 3.1415 |
||||
|
prefix = "Go_" |
||||
|
) |
||||
|
|
||||
|
var( |
||||
|
i int |
||||
|
pi float32 |
||||
|
prefix string |
||||
|
) |
||||
|
``` |
||||
|
### iota枚举 |
||||
|
|
||||
|
Go里面有一个关键字`iota`,这个关键字用来声明`enum`的时候采用,它默认开始值是0,const中每增加一行加1: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
x = iota // x == 0 |
||||
|
y = iota // y == 1 |
||||
|
z = iota // z == 2 |
||||
|
w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota" |
||||
|
) |
||||
|
|
||||
|
const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0 |
||||
|
|
||||
|
const ( |
||||
|
h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同 |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
a = iota //a=0 |
||||
|
b = "B" |
||||
|
c = iota //c=2 |
||||
|
d, e, f = iota, iota, iota //d=3,e=3,f=3 |
||||
|
g = iota //g = 4 |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v) |
||||
|
} |
||||
|
``` |
||||
|
>除非被显式设置为其它值或`iota`,每个`const`分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是`iota`,则它也被设置为`iota`。 |
||||
|
|
||||
|
### Go程序设计的一些规则 |
||||
|
Go之所以会那么简洁,是因为它有一些默认的行为: |
||||
|
- 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。 |
||||
|
- 大写字母开头的函数也是一样,相当于`class`中的带`public`关键词的公有函数;小写字母开头的就是有`private`关键词的私有函数。 |
||||
|
|
||||
|
## array、slice、map |
||||
|
|
||||
|
### array |
||||
|
`array`就是数组,它的定义方式如下: |
||||
|
```Go |
||||
|
|
||||
|
var arr [n]type |
||||
|
``` |
||||
|
在`[n]type`中,`n`表示数组的长度,`type`表示存储元素的类型。对数组的操作和其它语言类似,都是通过`[]`来进行读取或赋值: |
||||
|
```Go |
||||
|
|
||||
|
var arr [10]int // 声明了一个int类型的数组 |
||||
|
arr[0] = 42 // 数组下标是从0开始的 |
||||
|
arr[1] = 13 // 赋值操作 |
||||
|
fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42 |
||||
|
fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0 |
||||
|
``` |
||||
|
由于长度也是数组类型的一部分,因此`[3]int`与`[4]int`是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的`slice`类型了。 |
||||
|
|
||||
|
数组可以使用另一种`:=`来声明 |
||||
|
```Go |
||||
|
|
||||
|
a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组 |
||||
|
|
||||
|
b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0 |
||||
|
|
||||
|
c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度 |
||||
|
``` |
||||
|
也许你会说,我想数组里面的值还是数组,能实现吗?当然咯,Go支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组: |
||||
|
```Go |
||||
|
|
||||
|
// 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 |
||||
|
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}} |
||||
|
|
||||
|
// 上面的声明可以简化,直接忽略内部的类型 |
||||
|
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}} |
||||
|
``` |
||||
|
数组的分配如下所示: |
||||
|
|
||||
|
![](images/2.2.array.png?raw=true) |
||||
|
|
||||
|
图2.2 多维数组的映射关系 |
||||
|
|
||||
|
|
||||
|
### slice |
||||
|
|
||||
|
在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要“动态数组”。在Go里面这种数据结构叫`slice` |
||||
|
|
||||
|
`slice`并不是真正意义上的动态数组,而是一个引用类型。`slice`总是指向一个底层`array`,`slice`的声明也可以像`array`一样,只是不需要长度。 |
||||
|
```Go |
||||
|
|
||||
|
// 和声明array一样,只是少了长度 |
||||
|
var fslice []int |
||||
|
``` |
||||
|
接下来我们可以声明一个`slice`,并初始化数据,如下所示: |
||||
|
```Go |
||||
|
|
||||
|
slice := []byte {'a', 'b', 'c', 'd'} |
||||
|
``` |
||||
|
`slice`可以从一个数组或一个已经存在的`slice`中再次声明。`slice`通过`array[i:j]`来获取,其中`i`是数组的开始位置,`j`是结束位置,但不包含`array[j]`,它的长度是`j-i`。 |
||||
|
```Go |
||||
|
|
||||
|
// 声明一个含有10个元素元素类型为byte的数组 |
||||
|
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} |
||||
|
|
||||
|
// 声明两个含有byte的slice |
||||
|
var a, b []byte |
||||
|
|
||||
|
// a指向数组的第3个元素开始,并到第五个元素结束, |
||||
|
a = ar[2:5] |
||||
|
//现在a含有的元素: ar[2]、ar[3]和ar[4] |
||||
|
|
||||
|
// b是数组ar的另一个slice |
||||
|
b = ar[3:5] |
||||
|
// b的元素是:ar[3]和ar[4] |
||||
|
``` |
||||
|
>注意`slice`和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用`...`自动计算长度,而声明`slice`时,方括号内没有任何字符。 |
||||
|
|
||||
|
它们的数据结构如下所示 |
||||
|
|
||||
|
![](images/2.2.slice.png?raw=true) |
||||
|
|
||||
|
图2.3 slice和array的对应关系图 |
||||
|
|
||||
|
slice有一些简便的操作 |
||||
|
|
||||
|
- `slice`的默认开始位置是0,`ar[:n]`等价于`ar[0:n]` |
||||
|
- `slice`的第二个序列默认是数组的长度,`ar[n:]`等价于`ar[n:len(ar)]` |
||||
|
- 如果从一个数组里面直接获取`slice`,可以这样`ar[:]`,因为默认第一个序列是0,第二个是数组的长度,即等价于`ar[0:len(ar)]` |
||||
|
|
||||
|
下面这个例子展示了更多关于`slice`的操作: |
||||
|
```Go |
||||
|
|
||||
|
// 声明一个数组 |
||||
|
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} |
||||
|
// 声明两个slice |
||||
|
var aSlice, bSlice []byte |
||||
|
|
||||
|
// 演示一些简便操作 |
||||
|
aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c |
||||
|
aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j |
||||
|
aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素 |
||||
|
|
||||
|
// 从slice中获取slice |
||||
|
aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7 |
||||
|
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f |
||||
|
bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f |
||||
|
bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h |
||||
|
bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g |
||||
|
``` |
||||
|
`slice`是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的`aSlice`和`bSlice`,如果修改了`aSlice`中元素的值,那么`bSlice`相对应的值也会改变。 |
||||
|
|
||||
|
从概念上面来说`slice`像一个结构体,这个结构体包含了三个元素: |
||||
|
- 一个指针,指向数组中`slice`指定的开始位置 |
||||
|
- 长度,即`slice`的长度 |
||||
|
- 最大长度,也就是`slice`开始位置到数组的最后位置的长度 |
||||
|
```Go |
||||
|
|
||||
|
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} |
||||
|
Slice_a := Array_a[2:5] |
||||
|
``` |
||||
|
上面代码的真正存储结构如下图所示 |
||||
|
|
||||
|
![](images/2.2.slice2.png?raw=true) |
||||
|
|
||||
|
图2.4 slice对应数组的信息 |
||||
|
|
||||
|
对于`slice`有几个有用的内置函数: |
||||
|
|
||||
|
- `len` 获取`slice`的长度 |
||||
|
- `cap` 获取`slice`的最大容量 |
||||
|
- `append` 向`slice`里面追加一个或者多个元素,然后返回一个和`slice`一样类型的`slice` |
||||
|
- `copy` 函数`copy`从源`slice`的`src`中复制元素到目标`dst`,并且返回复制的元素的个数 |
||||
|
|
||||
|
注:`append`函数会改变`slice`所引用的数组的内容,从而影响到引用同一数组的其它`slice`。 |
||||
|
但当`slice`中没有剩余空间(即`(cap-len) == 0`)时,此时将动态分配新的数组空间。返回的`slice`数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的`slice`则不受影响。 |
||||
|
|
||||
|
从Go1.2开始slice支持了三个参数的slice,之前我们一直采用这种方式在slice或者array基础上来获取一个slice |
||||
|
```Go |
||||
|
|
||||
|
var array [10]int |
||||
|
slice := array[2:4] |
||||
|
``` |
||||
|
这个例子里面slice的容量是8,新版本里面可以指定这个容量 |
||||
|
|
||||
|
slice = array[2:4:7] |
||||
|
|
||||
|
上面这个的容量就是`7-2`,即5。这样这个产生的新的slice就没办法访问最后的三个元素。 |
||||
|
|
||||
|
如果slice是这样的形式`array[:i:j]`,即第一个参数为空,默认值就是0。 |
||||
|
|
||||
|
### map |
||||
|
|
||||
|
`map`也就是Python中字典的概念,它的格式为`map[keyType]valueType` |
||||
|
|
||||
|
我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`index`只能是`int`类型,而`map`多了很多类型,可以是`int`,可以是`string`及所有完全定义了`==`与`!=`操作的类型。 |
||||
|
```Go |
||||
|
|
||||
|
// 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化 |
||||
|
var numbers map[string]int |
||||
|
// 另一种map的声明方式 |
||||
|
numbers = make(map[string]int) |
||||
|
numbers["one"] = 1 //赋值 |
||||
|
numbers["ten"] = 10 //赋值 |
||||
|
numbers["three"] = 3 |
||||
|
|
||||
|
fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据 |
||||
|
// 打印出来如:第三个数字是: 3 |
||||
|
``` |
||||
|
|
||||
|
这个`map`就像我们平常看到的表格一样,左边列是`key`,右边列是值 |
||||
|
|
||||
|
使用map过程中需要注意的几点: |
||||
|
- `map`是无序的,每次打印出来的`map`都会不一样,它不能通过`index`获取,而必须通过`key`获取 |
||||
|
- `map`的长度是不固定的,也就是和`slice`一样,也是一种引用类型 |
||||
|
- 内置的`len`函数同样适用于`map`,返回`map`拥有的`key`的数量 |
||||
|
- `map`的值可以很方便的修改,通过`numbers["one"]=11`可以很容易的把key为`one`的字典值改为`11` |
||||
|
- `map`和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制 |
||||
|
|
||||
|
`map`的初始化可以通过`key:val`的方式初始化值,同时`map`内置有判断是否存在`key`的方式 |
||||
|
|
||||
|
通过`delete`删除`map`的元素: |
||||
|
|
||||
|
```Go |
||||
|
|
||||
|
// 初始化一个字典 |
||||
|
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 } |
||||
|
// map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true |
||||
|
csharpRating, ok := rating["C#"] |
||||
|
if ok { |
||||
|
fmt.Println("C# is in the map and its rating is ", csharpRating) |
||||
|
} else { |
||||
|
fmt.Println("We have no rating associated with C# in the map") |
||||
|
} |
||||
|
|
||||
|
delete(rating, "C") // 删除key为C的元素 |
||||
|
|
||||
|
``` |
||||
|
上面说过了,`map`也是一种引用类型,如果两个`map`同时指向一个底层,那么一个改变,另一个也相应的改变: |
||||
|
```Go |
||||
|
|
||||
|
m := make(map[string]string) |
||||
|
m["Hello"] = "Bonjour" |
||||
|
m1 := m |
||||
|
m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了 |
||||
|
|
||||
|
``` |
||||
|
### make、new操作 |
||||
|
|
||||
|
`make`用于内建类型(`map`、`slice` 和`channel`)的内存分配。`new`用于各种类型的内存分配。 |
||||
|
|
||||
|
内建函数`new`本质上说跟其它语言中的同名函数功能一样:`new(T)`分配了零值填充的`T`类型的内存空间,并且返回其地址,即一个`*T`类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型`T`的零值。有一点非常重要: |
||||
|
|
||||
|
>`new`返回指针。 |
||||
|
|
||||
|
内建函数`make(T, args)`与`new(T)`有着不同的功能,make只能创建`slice`、`map`和`channel`,并且返回一个有初始值(非零)的`T`类型,而不是`*T`。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个`slice`,是一个包含指向数据(内部`array`)的指针、长度和容量的三项描述符;在这些项目被初始化之前,`slice`为`nil`。对于`slice`、`map`和`channel`来说,`make`初始化了内部的数据结构,填充适当的值。 |
||||
|
|
||||
|
>`make`返回初始化后的(非零)值。 |
||||
|
|
||||
|
下面这个图详细的解释了`new`和`make`之间的区别。 |
||||
|
|
||||
|
![](images/2.2.makenew.png?raw=true) |
||||
|
|
||||
|
图2.5 make和new对应底层的内存分配 |
||||
|
|
||||
|
|
||||
|
## 零值 |
||||
|
关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 |
||||
|
此处罗列 部分类型 的 “零值” |
||||
|
```Go |
||||
|
|
||||
|
int 0 |
||||
|
int8 0 |
||||
|
int32 0 |
||||
|
int64 0 |
||||
|
uint 0x0 |
||||
|
rune 0 //rune的实际类型是 int32 |
||||
|
byte 0x0 // byte的实际类型是 uint8 |
||||
|
float32 0 //长度为 4 byte |
||||
|
float64 0 //长度为 8 byte |
||||
|
bool false |
||||
|
string "" |
||||
|
|
||||
|
``` |
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [你好,Go](<02.1.md>) |
||||
|
* 下一节: [流程和函数](<02.3.md>) |
@ -0,0 +1,559 @@ |
|||||
|
# 2.3 流程和函数 |
||||
|
这小节我们要介绍Go里面的流程控制以及函数操作。 |
||||
|
## 流程控制 |
||||
|
流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。Go中流程控制分三大类:条件判断,循环控制和无条件跳转。 |
||||
|
### if |
||||
|
`if`也许是各种编程语言中最常见的了,它的语法概括起来就是:如果满足条件就做某事,否则做另一件事。 |
||||
|
|
||||
|
Go里面`if`条件判断语句中不需要括号,如下代码所示 |
||||
|
```Go |
||||
|
|
||||
|
if x > 10 { |
||||
|
fmt.Println("x is greater than 10") |
||||
|
} else { |
||||
|
fmt.Println("x is less than 10") |
||||
|
} |
||||
|
``` |
||||
|
Go的`if`还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示 |
||||
|
```Go |
||||
|
|
||||
|
// 计算获取值x,然后根据x返回的大小,判断是否大于10。 |
||||
|
if x := computedValue(); x > 10 { |
||||
|
fmt.Println("x is greater than 10") |
||||
|
} else { |
||||
|
fmt.Println("x is less than 10") |
||||
|
} |
||||
|
|
||||
|
//这个地方如果这样调用就编译出错了,因为x是条件里面的变量 |
||||
|
fmt.Println(x) |
||||
|
``` |
||||
|
多个条件的时候如下所示: |
||||
|
```Go |
||||
|
|
||||
|
if integer == 3 { |
||||
|
fmt.Println("The integer is equal to 3") |
||||
|
} else if integer < 3 { |
||||
|
fmt.Println("The integer is less than 3") |
||||
|
} else { |
||||
|
fmt.Println("The integer is greater than 3") |
||||
|
} |
||||
|
``` |
||||
|
### goto |
||||
|
|
||||
|
Go有`goto`语句——请明智地使用它。用`goto`跳转到必须在当前函数内定义的标签。例如假设这样一个循环: |
||||
|
```Go |
||||
|
|
||||
|
func myFunc() { |
||||
|
i := 0 |
||||
|
Here: //这行的第一个词,以冒号结束作为标签 |
||||
|
println(i) |
||||
|
i++ |
||||
|
goto Here //跳转到Here去 |
||||
|
} |
||||
|
``` |
||||
|
>标签名是大小写敏感的。 |
||||
|
|
||||
|
### for |
||||
|
Go里面最强大的一个控制逻辑就是`for`,它既可以用来循环读取数据,又可以当作`while`来控制逻辑,还能迭代操作。它的语法如下: |
||||
|
```Go |
||||
|
|
||||
|
for expression1; expression2; expression3 { |
||||
|
//... |
||||
|
} |
||||
|
``` |
||||
|
`expression1`、`expression2`和`expression3`都是表达式,其中`expression1`和`expression3`是变量声明或者函数调用返回值之类的,`expression2`是用来条件判断,`expression1`在循环开始之前调用,`expression3`在每轮循环结束之时调用。 |
||||
|
|
||||
|
一个例子比上面讲那么多更有用,那么我们看看下面的例子吧: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
func main(){ |
||||
|
sum := 0; |
||||
|
for index:=0; index < 10 ; index++ { |
||||
|
sum += index |
||||
|
} |
||||
|
fmt.Println("sum is equal to ", sum) |
||||
|
} |
||||
|
// 输出:sum is equal to 45 |
||||
|
``` |
||||
|
有些时候需要进行多个赋值操作,由于Go里面没有`,`操作符,那么可以使用平行赋值`i, j = i+1, j-1` |
||||
|
|
||||
|
|
||||
|
有些时候如果我们忽略`expression1`和`expression3`: |
||||
|
```Go |
||||
|
|
||||
|
sum := 1 |
||||
|
for ; sum < 1000; { |
||||
|
sum += sum |
||||
|
} |
||||
|
``` |
||||
|
其中`;`也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是`while`的功能。 |
||||
|
```Go |
||||
|
|
||||
|
sum := 1 |
||||
|
for sum < 1000 { |
||||
|
sum += sum |
||||
|
} |
||||
|
``` |
||||
|
在循环里面有两个关键操作`break`和`continue` ,`break`操作是跳出当前循环,`continue`是跳过本次循环。当嵌套过深的时候,`break`可以配合标签使用,即跳转至标签所指定的位置,详细参考如下例子: |
||||
|
```Go |
||||
|
|
||||
|
for index := 10; index>0; index-- { |
||||
|
if index == 5{ |
||||
|
break // 或者continue |
||||
|
} |
||||
|
fmt.Println(index) |
||||
|
} |
||||
|
// break打印出来10、9、8、7、6 |
||||
|
// continue打印出来10、9、8、7、6、4、3、2、1 |
||||
|
``` |
||||
|
`break`和`continue`还可以跟着标号,用来跳到多重循环中的外层循环 |
||||
|
|
||||
|
`for`配合`range`可以用于读取`slice`和`map`的数据: |
||||
|
```Go |
||||
|
|
||||
|
for k,v:=range map { |
||||
|
fmt.Println("map's key:",k) |
||||
|
fmt.Println("map's val:",v) |
||||
|
} |
||||
|
``` |
||||
|
由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用`_`来丢弃不需要的返回值 |
||||
|
例如 |
||||
|
```Go |
||||
|
|
||||
|
for _, v := range map{ |
||||
|
fmt.Println("map's val:", v) |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
### switch |
||||
|
有些时候你需要写很多的`if-else`来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候`switch`就能很好的解决这个问题。它的语法如下 |
||||
|
```Go |
||||
|
|
||||
|
switch sExpr { |
||||
|
case expr1: |
||||
|
some instructions |
||||
|
case expr2: |
||||
|
some other instructions |
||||
|
case expr3: |
||||
|
some other instructions |
||||
|
default: |
||||
|
other code |
||||
|
} |
||||
|
``` |
||||
|
`sExpr`和`expr1`、`expr2`、`expr3`的类型必须一致。Go的`switch`非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果`switch`没有表达式,它会匹配`true`。 |
||||
|
```Go |
||||
|
|
||||
|
i := 10 |
||||
|
switch i { |
||||
|
case 1: |
||||
|
fmt.Println("i is equal to 1") |
||||
|
case 2, 3, 4: |
||||
|
fmt.Println("i is equal to 2, 3 or 4") |
||||
|
case 10: |
||||
|
fmt.Println("i is equal to 10") |
||||
|
default: |
||||
|
fmt.Println("All I know is that i is an integer") |
||||
|
} |
||||
|
``` |
||||
|
在第5行中,我们把很多值聚合在了一个`case`里面,同时,Go里面`switch`默认相当于每个`case`最后带有`break`,匹配成功后不会自动向下执行其他case,而是跳出整个`switch`, 但是可以使用`fallthrough`强制执行后面的case代码。 |
||||
|
```Go |
||||
|
|
||||
|
integer := 6 |
||||
|
switch integer { |
||||
|
case 4: |
||||
|
fmt.Println("The integer was <= 4") |
||||
|
fallthrough |
||||
|
case 5: |
||||
|
fmt.Println("The integer was <= 5") |
||||
|
fallthrough |
||||
|
case 6: |
||||
|
fmt.Println("The integer was <= 6") |
||||
|
fallthrough |
||||
|
case 7: |
||||
|
fmt.Println("The integer was <= 7") |
||||
|
fallthrough |
||||
|
case 8: |
||||
|
fmt.Println("The integer was <= 8") |
||||
|
fallthrough |
||||
|
default: |
||||
|
fmt.Println("default case") |
||||
|
} |
||||
|
``` |
||||
|
上面的程序将输出 |
||||
|
```Go |
||||
|
|
||||
|
The integer was <= 6 |
||||
|
The integer was <= 7 |
||||
|
The integer was <= 8 |
||||
|
default case |
||||
|
|
||||
|
``` |
||||
|
## 函数 |
||||
|
函数是Go里面的核心设计,它通过关键字`func`来声明,它的格式如下: |
||||
|
```Go |
||||
|
|
||||
|
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { |
||||
|
//这里是处理逻辑代码 |
||||
|
//返回多个值 |
||||
|
return value1, value2 |
||||
|
} |
||||
|
``` |
||||
|
上面的代码我们看出 |
||||
|
|
||||
|
- 关键字`func`用来声明一个函数`funcName` |
||||
|
- 函数可以有一个或者多个参数,每个参数后面带有类型,通过`,`分隔 |
||||
|
- 函数可以返回多个值 |
||||
|
- 上面返回值声明了两个变量`output1`和`output2`,如果你不想声明也可以,直接就两个类型 |
||||
|
- 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号 |
||||
|
- 如果没有返回值,那么就直接省略最后的返回信息 |
||||
|
- 如果有返回值, 那么必须在函数的外层添加return语句 |
||||
|
|
||||
|
下面我们来看一个实际应用函数的例子(用来计算Max值) |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
// 返回a、b中最大值. |
||||
|
func max(a, b int) int { |
||||
|
if a > b { |
||||
|
return a |
||||
|
} |
||||
|
return b |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
x := 3 |
||||
|
y := 4 |
||||
|
z := 5 |
||||
|
|
||||
|
max_xy := max(x, y) //调用函数max(x, y) |
||||
|
max_xz := max(x, z) //调用函数max(x, z) |
||||
|
|
||||
|
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy) |
||||
|
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz) |
||||
|
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它 |
||||
|
} |
||||
|
``` |
||||
|
上面这个里面我们可以看到`max`函数有两个参数,它们的类型都是`int`,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时我们注意到它的返回值就是一个类型,这个就是省略写法。 |
||||
|
|
||||
|
### 多个返回值 |
||||
|
Go语言比C更先进的特性,其中一点就是函数能够返回多个值。 |
||||
|
|
||||
|
我们直接上代码看例子 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
//返回 A+B 和 A*B |
||||
|
func SumAndProduct(A, B int) (int, int) { |
||||
|
return A+B, A*B |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
x := 3 |
||||
|
y := 4 |
||||
|
|
||||
|
xPLUSy, xTIMESy := SumAndProduct(x, y) |
||||
|
|
||||
|
fmt.Printf("%d + %d = %d\n", x, y, xPLUSy) |
||||
|
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy) |
||||
|
} |
||||
|
``` |
||||
|
上面的例子我们可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果你的函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。 |
||||
|
```Go |
||||
|
|
||||
|
func SumAndProduct(A, B int) (add int, Multiplied int) { |
||||
|
add = A+B |
||||
|
Multiplied = A*B |
||||
|
return |
||||
|
} |
||||
|
``` |
||||
|
### 变参 |
||||
|
Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参: |
||||
|
```Go |
||||
|
|
||||
|
func myfunc(arg ...int) {} |
||||
|
``` |
||||
|
`arg ...int`告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是`int`。在函数体中,变量`arg`是一个`int`的`slice`: |
||||
|
```Go |
||||
|
|
||||
|
for _, n := range arg { |
||||
|
fmt.Printf("And the number is: %d\n", n) |
||||
|
} |
||||
|
``` |
||||
|
### 传值与传指针 |
||||
|
当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。 |
||||
|
|
||||
|
为了验证我们上面的说法,我们来看一个例子 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
//简单的一个函数,实现了参数+1的操作 |
||||
|
func add1(a int) int { |
||||
|
a = a+1 // 我们改变了a的值 |
||||
|
return a //返回一个新值 |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
x := 3 |
||||
|
|
||||
|
fmt.Println("x = ", x) // 应该输出 "x = 3" |
||||
|
|
||||
|
x1 := add1(x) //调用add1(x) |
||||
|
|
||||
|
fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4" |
||||
|
fmt.Println("x = ", x) // 应该输出"x = 3" |
||||
|
} |
||||
|
``` |
||||
|
看到了吗?虽然我们调用了`add1`函数,并且在`add1`中执行`a = a+1`操作,但是上面例子中`x`变量的值没有发生变化 |
||||
|
|
||||
|
理由很简单:因为当我们调用`add1`的时候,`add1`接收的参数其实是`x`的copy,而不是`x`本身。 |
||||
|
|
||||
|
那你也许会问了,如果真的需要传这个`x`本身,该怎么办呢? |
||||
|
|
||||
|
这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有`add1`函数知道`x`变量所在的地址,才能修改`x`变量的值。所以我们需要将`x`所在地址`&x`传入函数,并将函数的参数的类型由`int`改为`*int`,即改为指针类型,才能在函数中修改`x`变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
//简单的一个函数,实现了参数+1的操作 |
||||
|
func add1(a *int) int { // 请注意, |
||||
|
*a = *a+1 // 修改了a的值 |
||||
|
return *a // 返回新值 |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
x := 3 |
||||
|
|
||||
|
fmt.Println("x = ", x) // 应该输出 "x = 3" |
||||
|
|
||||
|
x1 := add1(&x) // 调用 add1(&x) 传x的地址 |
||||
|
|
||||
|
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4" |
||||
|
fmt.Println("x = ", x) // 应该输出 "x = 4" |
||||
|
} |
||||
|
``` |
||||
|
这样,我们就达到了修改`x`的目的。那么到底传指针有什么好处呢? |
||||
|
|
||||
|
- 传指针使得多个函数能操作同一个对象。 |
||||
|
- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。 |
||||
|
- Go语言中`channel`,`slice`,`map`这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变`slice`的长度,则仍需要取地址传递指针) |
||||
|
|
||||
|
### defer |
||||
|
Go语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的: |
||||
|
```Go |
||||
|
|
||||
|
func ReadWrite() bool { |
||||
|
file.Open("file") |
||||
|
// 做一些工作 |
||||
|
if failureX { |
||||
|
file.Close() |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if failureY { |
||||
|
file.Close() |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
file.Close() |
||||
|
return true |
||||
|
} |
||||
|
``` |
||||
|
我们看到上面有很多重复的代码,Go的`defer`有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在`defer`后指定的函数会在函数退出前调用。 |
||||
|
```Go |
||||
|
|
||||
|
func ReadWrite() bool { |
||||
|
file.Open("file") |
||||
|
defer file.Close() |
||||
|
if failureX { |
||||
|
return false |
||||
|
} |
||||
|
if failureY { |
||||
|
return false |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
``` |
||||
|
如果有很多调用`defer`,那么`defer`是采用后进先出模式,所以如下代码会输出`4 3 2 1 0` |
||||
|
```Go |
||||
|
|
||||
|
for i := 0; i < 5; i++ { |
||||
|
defer fmt.Printf("%d ", i) |
||||
|
} |
||||
|
``` |
||||
|
### 函数作为值、类型 |
||||
|
|
||||
|
在Go中函数也是一种变量,我们可以通过`type`来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型 |
||||
|
|
||||
|
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...]) |
||||
|
|
||||
|
函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
type testInt func(int) bool // 声明了一个函数类型 |
||||
|
|
||||
|
func isOdd(integer int) bool { |
||||
|
if integer%2 == 0 { |
||||
|
return false |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
func isEven(integer int) bool { |
||||
|
if integer%2 == 0 { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 声明的函数类型在这个地方当做了一个参数 |
||||
|
|
||||
|
func filter(slice []int, f testInt) []int { |
||||
|
var result []int |
||||
|
for _, value := range slice { |
||||
|
if f(value) { |
||||
|
result = append(result, value) |
||||
|
} |
||||
|
} |
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
func main(){ |
||||
|
slice := []int {1, 2, 3, 4, 5, 7} |
||||
|
fmt.Println("slice = ", slice) |
||||
|
odd := filter(slice, isOdd) // 函数当做值来传递了 |
||||
|
fmt.Println("Odd elements of slice are: ", odd) |
||||
|
even := filter(slice, isEven) // 函数当做值来传递了 |
||||
|
fmt.Println("Even elements of slice are: ", even) |
||||
|
} |
||||
|
``` |
||||
|
函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到`testInt`这个类型是一个函数类型,然后两个`filter`函数的参数和返回值与`testInt`类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。 |
||||
|
|
||||
|
### Panic和Recover |
||||
|
|
||||
|
Go没有像Java那样的异常机制,它不能抛出异常,而是使用了`panic`和`recover`机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有`panic`的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢? |
||||
|
|
||||
|
Panic |
||||
|
>是一个内建函数,可以中断原有的控制流程,进入一个`panic`状态中。当函数`F`调用`panic`,函数F的执行被中断,但是`F`中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,`F`的行为就像调用了`panic`。这一过程继续向上,直到发生`panic`的`goroutine`中所有调用的函数返回,此时程序退出。`panic`可以直接调用`panic`产生。也可以由运行时错误产生,例如访问越界的数组。 |
||||
|
|
||||
|
Recover |
||||
|
>是一个内建的函数,可以让进入`panic`状态的`goroutine`恢复过来。`recover`仅在延迟函数中有效。在正常的执行过程中,调用`recover`会返回`nil`,并且没有其它任何效果。如果当前的`goroutine`陷入`panic`状态,调用`recover`可以捕获到`panic`的输入值,并且恢复正常的执行。 |
||||
|
|
||||
|
下面这个函数演示了如何在过程中使用`panic` |
||||
|
```Go |
||||
|
|
||||
|
var user = os.Getenv("USER") |
||||
|
|
||||
|
func init() { |
||||
|
if user == "" { |
||||
|
panic("no value for $USER") |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
下面这个函数检查作为其参数的函数在执行时是否会产生`panic`: |
||||
|
```Go |
||||
|
|
||||
|
func throwsPanic(f func()) (b bool) { |
||||
|
defer func() { |
||||
|
if x := recover(); x != nil { |
||||
|
b = true |
||||
|
} |
||||
|
}() |
||||
|
f() //执行函数f,如果f中出现了panic,那么就可以恢复回来 |
||||
|
return |
||||
|
} |
||||
|
``` |
||||
|
### `main`函数和`init`函数 |
||||
|
|
||||
|
Go里面有两个保留的函数:`init`函数(能够应用于所有的`package`)和`main`函数(只能应用于`package main`)。这两个函数在定义时不能有任何的参数和返回值。虽然一个`package`里面可以写任意多个`init`函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个`package`中每个文件只写一个`init`函数。 |
||||
|
|
||||
|
Go程序会自动调用`init()`和`main()`,所以你不需要在任何地方调用这两个函数。每个`package`中的`init`函数都是可选的,但`package main`就必须包含一个`main`函数。 |
||||
|
|
||||
|
程序的初始化和执行都起始于`main`包。如果`main`包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到`fmt`包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行`init`函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对`main`包中的包级常量和变量进行初始化,然后执行`main`包中的`init`函数(如果存在的话),最后执行`main`函数。下图详细地解释了整个执行过程: |
||||
|
|
||||
|
![](images/2.3.init.png?raw=true) |
||||
|
|
||||
|
图2.6 main函数引入包初始化流程图 |
||||
|
|
||||
|
### import |
||||
|
我们在写Go代码的时候经常用到import这个命令用来导入包文件,而我们经常看到的方式参考如下: |
||||
|
```Go |
||||
|
|
||||
|
import( |
||||
|
"fmt" |
||||
|
) |
||||
|
``` |
||||
|
然后我们代码里面可以通过如下的方式调用 |
||||
|
```Go |
||||
|
|
||||
|
fmt.Println("hello world") |
||||
|
``` |
||||
|
上面这个fmt是Go语言的标准库,其实是去`GOROOT`环境变量指定目录下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块: |
||||
|
|
||||
|
1. 相对路径 |
||||
|
|
||||
|
import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import |
||||
|
|
||||
|
2. 绝对路径 |
||||
|
|
||||
|
import “shorturl/model” //加载gopath/src/shorturl/model模块 |
||||
|
|
||||
|
|
||||
|
上面展示了一些import常用的几种方式,但是还有一些特殊的import,让很多新手很费解,下面我们来一一讲解一下到底是怎么一回事 |
||||
|
|
||||
|
|
||||
|
1. 点操作 |
||||
|
|
||||
|
我们有时候会看到如下的方式导入包 |
||||
|
|
||||
|
import( |
||||
|
. "fmt" |
||||
|
) |
||||
|
|
||||
|
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world") |
||||
|
|
||||
|
2. 别名操作 |
||||
|
|
||||
|
别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字 |
||||
|
|
||||
|
import( |
||||
|
f "fmt" |
||||
|
) |
||||
|
|
||||
|
别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world") |
||||
|
|
||||
|
3. _操作 |
||||
|
|
||||
|
这个操作经常是让很多人费解的一个操作符,请看下面这个import |
||||
|
```Go |
||||
|
|
||||
|
import ( |
||||
|
"database/sql" |
||||
|
_ "github.com/ziutek/mymysql/godrv" |
||||
|
) |
||||
|
``` |
||||
|
_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。 |
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [Go基础](<02.2.md>) |
||||
|
* 下一节: [struct类型](<02.4.md>) |
@ -0,0 +1,226 @@ |
|||||
|
# 2.4 struct类型 |
||||
|
## struct |
||||
|
Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型`person`代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之`struct`。如下代码所示: |
||||
|
```Go |
||||
|
|
||||
|
type person struct { |
||||
|
name string |
||||
|
age int |
||||
|
} |
||||
|
``` |
||||
|
看到了吗?声明一个struct如此简单,上面的类型包含有两个字段 |
||||
|
- 一个string类型的字段name,用来保存用户名称这个属性 |
||||
|
- 一个int类型的字段age,用来保存用户年龄这个属性 |
||||
|
|
||||
|
如何使用struct呢?请看下面的代码 |
||||
|
```Go |
||||
|
|
||||
|
type person struct { |
||||
|
name string |
||||
|
age int |
||||
|
} |
||||
|
|
||||
|
var P person // P现在就是person类型的变量了 |
||||
|
|
||||
|
P.name = "Astaxie" // 赋值"Astaxie"给P的name属性. |
||||
|
P.age = 25 // 赋值"25"给变量P的age属性 |
||||
|
fmt.Printf("The person's name is %s", P.name) // 访问P的name属性. |
||||
|
``` |
||||
|
除了上面这种P的声明使用之外,还有另外几种声明使用方式: |
||||
|
|
||||
|
- 1.按照顺序提供初始化值 |
||||
|
|
||||
|
P := person{"Tom", 25} |
||||
|
|
||||
|
- 2.通过`field:value`的方式初始化,这样可以任意顺序 |
||||
|
|
||||
|
P := person{age:24, name:"Tom"} |
||||
|
|
||||
|
- 3.当然也可以通过`new`函数分配一个指针,此处P的类型为*person |
||||
|
|
||||
|
P := new(person) |
||||
|
|
||||
|
下面我们看一个完整的使用struct的例子 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
// 声明一个新的类型 |
||||
|
type person struct { |
||||
|
name string |
||||
|
age int |
||||
|
} |
||||
|
|
||||
|
// 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差 |
||||
|
// struct也是传值的 |
||||
|
func Older(p1, p2 person) (person, int) { |
||||
|
if p1.age>p2.age { // 比较p1和p2这两个人的年龄 |
||||
|
return p1, p1.age-p2.age |
||||
|
} |
||||
|
return p2, p2.age-p1.age |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
var tom person |
||||
|
|
||||
|
// 赋值初始化 |
||||
|
tom.name, tom.age = "Tom", 18 |
||||
|
|
||||
|
// 两个字段都写清楚的初始化 |
||||
|
bob := person{age:25, name:"Bob"} |
||||
|
|
||||
|
// 按照struct定义顺序初始化值 |
||||
|
paul := person{"Paul", 43} |
||||
|
|
||||
|
tb_Older, tb_diff := Older(tom, bob) |
||||
|
tp_Older, tp_diff := Older(tom, paul) |
||||
|
bp_Older, bp_diff := Older(bob, paul) |
||||
|
|
||||
|
fmt.Printf("Of %s and %s, %s is older by %d years\n", |
||||
|
tom.name, bob.name, tb_Older.name, tb_diff) |
||||
|
|
||||
|
fmt.Printf("Of %s and %s, %s is older by %d years\n", |
||||
|
tom.name, paul.name, tp_Older.name, tp_diff) |
||||
|
|
||||
|
fmt.Printf("Of %s and %s, %s is older by %d years\n", |
||||
|
bob.name, paul.name, bp_Older.name, bp_diff) |
||||
|
} |
||||
|
``` |
||||
|
### struct的匿名字段 |
||||
|
我们上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。 |
||||
|
|
||||
|
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。 |
||||
|
|
||||
|
让我们来看一个例子,让上面说的这些更具体化 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
type Human struct { |
||||
|
name string |
||||
|
age int |
||||
|
weight int |
||||
|
} |
||||
|
|
||||
|
type Student struct { |
||||
|
Human // 匿名字段,那么默认Student就包含了Human的所有字段 |
||||
|
speciality string |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
// 我们初始化一个学生 |
||||
|
mark := Student{Human{"Mark", 25, 120}, "Computer Science"} |
||||
|
|
||||
|
// 我们访问相应的字段 |
||||
|
fmt.Println("His name is ", mark.name) |
||||
|
fmt.Println("His age is ", mark.age) |
||||
|
fmt.Println("His weight is ", mark.weight) |
||||
|
fmt.Println("His speciality is ", mark.speciality) |
||||
|
// 修改对应的备注信息 |
||||
|
mark.speciality = "AI" |
||||
|
fmt.Println("Mark changed his speciality") |
||||
|
fmt.Println("His speciality is ", mark.speciality) |
||||
|
// 修改他的年龄信息 |
||||
|
fmt.Println("Mark become old") |
||||
|
mark.age = 46 |
||||
|
fmt.Println("His age is", mark.age) |
||||
|
// 修改他的体重信息 |
||||
|
fmt.Println("Mark is not an athlet anymore") |
||||
|
mark.weight += 60 |
||||
|
fmt.Println("His weight is", mark.weight) |
||||
|
} |
||||
|
``` |
||||
|
图例如下: |
||||
|
|
||||
|
![](images/2.4.student_struct.png?raw=true) |
||||
|
|
||||
|
图2.7 struct组合,Student组合了Human struct和string基本类型 |
||||
|
|
||||
|
我们看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,对,匿名字段就是这样,能够实现字段的继承。是不是很酷啊?还有比这个更酷的呢,那就是student还能访问Human这个字段作为字段名。请看下面的代码,是不是更酷了。 |
||||
|
```Go |
||||
|
|
||||
|
mark.Human = Human{"Marcus", 55, 220} |
||||
|
mark.Human.age -= 1 |
||||
|
``` |
||||
|
通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
type Skills []string |
||||
|
|
||||
|
type Human struct { |
||||
|
name string |
||||
|
age int |
||||
|
weight int |
||||
|
} |
||||
|
|
||||
|
type Student struct { |
||||
|
Human // 匿名字段,struct |
||||
|
Skills // 匿名字段,自定义的类型string slice |
||||
|
int // 内置类型作为匿名字段 |
||||
|
speciality string |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
// 初始化学生Jane |
||||
|
jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"} |
||||
|
// 现在我们来访问相应的字段 |
||||
|
fmt.Println("Her name is ", jane.name) |
||||
|
fmt.Println("Her age is ", jane.age) |
||||
|
fmt.Println("Her weight is ", jane.weight) |
||||
|
fmt.Println("Her speciality is ", jane.speciality) |
||||
|
// 我们来修改他的skill技能字段 |
||||
|
jane.Skills = []string{"anatomy"} |
||||
|
fmt.Println("Her skills are ", jane.Skills) |
||||
|
fmt.Println("She acquired two new ones ") |
||||
|
jane.Skills = append(jane.Skills, "physics", "golang") |
||||
|
fmt.Println("Her skills now are ", jane.Skills) |
||||
|
// 修改匿名内置类型字段 |
||||
|
jane.int = 3 |
||||
|
fmt.Println("Her preferred number is", jane.int) |
||||
|
} |
||||
|
``` |
||||
|
从上面例子我们看出来struct不仅仅能够将struct作为匿名字段,自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如例子中的append)。 |
||||
|
|
||||
|
这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢? |
||||
|
|
||||
|
Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过`student.phone`访问的时候,是访问student里面的字段,而不是human里面的字段。 |
||||
|
|
||||
|
这样就允许我们去重载通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
type Human struct { |
||||
|
name string |
||||
|
age int |
||||
|
phone string // Human类型拥有的字段 |
||||
|
} |
||||
|
|
||||
|
type Employee struct { |
||||
|
Human // 匿名字段Human |
||||
|
speciality string |
||||
|
phone string // 雇员的phone字段 |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"} |
||||
|
fmt.Println("Bob's work phone is:", Bob.phone) |
||||
|
// 如果我们要访问Human的phone字段 |
||||
|
fmt.Println("Bob's personal phone is:", Bob.Human.phone) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [流程和函数](<02.3.md>) |
||||
|
* 下一节: [面向对象](<02.5.md>) |
@ -0,0 +1,160 @@ |
|||||
|
# 3.1 Web工作方式 |
||||
|
|
||||
|
我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢? |
||||
|
|
||||
|
对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。 |
||||
|
|
||||
|
![](images/3.1.web2.png?raw=true) |
||||
|
|
||||
|
图3.1 用户访问一个Web站点的过程 |
||||
|
|
||||
|
一个Web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。 |
||||
|
|
||||
|
Web服务器的工作原理可以简单地归纳为: |
||||
|
|
||||
|
- 客户机通过TCP/IP协议建立到服务器的TCP连接 |
||||
|
- 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档 |
||||
|
- 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端 |
||||
|
- 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果 |
||||
|
|
||||
|
一个简单的HTTP事务就是这样实现的,看起来很复杂,原理其实是挺简单的。需要注意的是客户机与服务器之间的通信是非持久连接的,也就是当服务器发送了应答后就与客户机断开连接,等待下一次请求。 |
||||
|
|
||||
|
## URL和DNS解析 |
||||
|
我们浏览网页都是通过URL访问的,那么URL到底是怎么样的呢? |
||||
|
|
||||
|
URL(Uniform Resource Locator)是“统一资源定位符”的英文缩写,用于描述一个网络上的资源, 基本格式如下 |
||||
|
|
||||
|
scheme://host[:port#]/path/.../[?query-string][#anchor] |
||||
|
scheme 指定底层使用的协议(例如:http, https, ftp) |
||||
|
host HTTP服务器的IP地址或者域名 |
||||
|
port# HTTP服务器的默认端口是80,这种情况下端口号可以省略。如果使用了别的端口,必须指明,例如 http://www.cnblogs.com:8080/ |
||||
|
path 访问资源的路径 |
||||
|
query-string 发送给http服务器的数据 |
||||
|
anchor 锚 |
||||
|
|
||||
|
DNS(Domain Name System)是“域名系统”的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,它用于TCP/IP网络,它从事将主机名或域名转换为实际IP地址的工作。DNS就是这样的一位“翻译官”,它的基本工作原理可用下图来表示。 |
||||
|
|
||||
|
![](images/3.1.dns_hierachy.png?raw=true) |
||||
|
|
||||
|
图3.2 DNS工作原理 |
||||
|
|
||||
|
更详细的DNS解析的过程如下,这个过程有助于我们理解DNS的工作模式 |
||||
|
|
||||
|
1. 在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。 |
||||
|
|
||||
|
2. 如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。 |
||||
|
|
||||
|
3. 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。 |
||||
|
|
||||
|
4. 如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。 |
||||
|
|
||||
|
5. 如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至 “根DNS服务器”,“根DNS服务器”收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找qq.com域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机。 |
||||
|
|
||||
|
6. 如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管本地DNS服务器用的是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。 |
||||
|
|
||||
|
![](images/3.1.dns_inquery.png?raw=true) |
||||
|
|
||||
|
图3.3 DNS解析的整个流程 |
||||
|
|
||||
|
> 所谓 `递归查询过程` 就是 “查询的递交者” 更替, 而 `迭代查询过程` 则是 “查询的递交者”不变。 |
||||
|
> |
||||
|
> 举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长,学长告诉他,这姑娘是xx系的;然后这哥们儿马不停蹄又问了xx系的办公室主任助理同学,助理同学说是xx系yy班的,然后很仗义的哥们儿去xx系yy班的班长那里取到了该女孩儿电话。(此处完成若干次迭代查询,即,问询者角色不变,但反复更替问询对象)最后,他把号码交到了你手里。完成整个查询过程。 |
||||
|
|
||||
|
通过上面的步骤,我们最后获取的是IP地址,也就是浏览器最后发起请求的时候是基于IP来和服务器做信息交互的。 |
||||
|
|
||||
|
## HTTP协议详解 |
||||
|
|
||||
|
HTTP协议是Web工作的核心,所以要了解清楚Web的工作方式就需要详细的了解清楚HTTP是怎么样工作的。 |
||||
|
|
||||
|
HTTP是一种让Web服务器与浏览器(客户端)通过Internet发送与接收数据的协议,它建立在TCP协议之上,一般采用TCP的80端口。它是一个请求、响应协议--客户端发出一个请求,服务器响应这个请求。在HTTP中,客户端总是通过建立一个连接与发送一个HTTP请求来发起一个事务。服务器不能主动去与客户端联系,也不能给客户端发出一个回调连接。客户端与服务器端都可以提前中断一个连接。例如,当浏览器下载一个文件时,你可以通过点击“停止”键来中断文件的下载,关闭与服务器的HTTP连接。 |
||||
|
|
||||
|
HTTP协议是无状态的,同一个客户端的这次请求和上次请求是没有对应关系的,对HTTP服务器来说,它并不知道这两个请求是否来自同一个客户端。为了解决这个问题, Web程序引入了Cookie机制来维护连接的可持续状态。 |
||||
|
|
||||
|
>HTTP协议是建立在TCP协议之上的,因此TCP攻击一样会影响HTTP的通讯,例如比较常见的一些攻击:SYN Flood是当前最流行的DoS(拒绝服务攻击)与DdoS(分布式拒绝服务攻击)的方式之一,这是一种利用TCP协议缺陷,发送大量伪造的TCP连接请求,从而使得被攻击方资源耗尽(CPU满负荷或内存不足)的攻击方式。 |
||||
|
|
||||
|
### HTTP请求包(浏览器信息) |
||||
|
|
||||
|
我们先来看看Request包的结构, Request包分为3部分,第一部分叫Request line(请求行), 第二部分叫Request header(请求头),第三部分是body(主体)。header和body之间有个空行,请求包的例子所示: |
||||
|
|
||||
|
GET /domains/example/ HTTP/1.1 //请求行: 请求方法 请求URI HTTP协议/协议版本 |
||||
|
Host:www.iana.org //服务端的主机名 |
||||
|
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息 |
||||
|
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的MIME |
||||
|
Accept-Encoding:gzip,deflate,sdch //是否支持流压缩 |
||||
|
Accept-Charset:UTF-8,*;q=0.5 //客户端字符编码集 |
||||
|
//空行,用于分割请求头和消息体 |
||||
|
//消息体,请求资源参数,例如POST传递的参数 |
||||
|
|
||||
|
HTTP协议定义了很多与服务器交互的请求方法,最基本的有4种,分别是GET,POST,PUT,DELETE。一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,增,改,删4个操作。我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息。 |
||||
|
|
||||
|
通过fiddler抓包可以看到如下请求信息: |
||||
|
|
||||
|
![](images/3.1.http.png?raw=true) |
||||
|
|
||||
|
图3.4 fiddler抓取的GET信息 |
||||
|
|
||||
|
![](images/3.1.httpPOST.png?raw=true) |
||||
|
|
||||
|
图3.5 fiddler抓取的POST信息 |
||||
|
|
||||
|
我们看看GET和POST的区别: |
||||
|
|
||||
|
1. 我们可以看到GET请求消息体为空,POST请求带有消息体。 |
||||
|
2. GET提交的数据会放在URL之后,以`?`分割URL和传输数据,参数之间以`&`相连,如`EditPosts.aspx?name=test1&id=123456`。POST方法是把提交的数据放在HTTP包的body中。 |
||||
|
3. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制。 |
||||
|
4. GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码。 |
||||
|
|
||||
|
### HTTP响应包(服务器信息) |
||||
|
我们再来看看HTTP的response包,他的结构如下: |
||||
|
|
||||
|
HTTP/1.1 200 OK //状态行 |
||||
|
Server: nginx/1.0.8 //服务器使用的WEB软件名及版本 |
||||
|
Date:Date: Tue, 30 Oct 2012 04:14:25 GMT //发送时间 |
||||
|
Content-Type: text/html //服务器发送信息的类型 |
||||
|
Transfer-Encoding: chunked //表示发送HTTP包是分段发的 |
||||
|
Connection: keep-alive //保持连接状态 |
||||
|
Content-Length: 90 //主体内容长度 |
||||
|
//空行 用来分割消息头和主体 |
||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... //消息体 |
||||
|
|
||||
|
Response包中的第一行叫做状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。 |
||||
|
|
||||
|
状态码用来告诉HTTP客户端,HTTP服务器是否产生了预期的Response。HTTP/1.1协议中定义了5类状态码, 状态码由三位数字组成,第一个数字定义了响应的类别 |
||||
|
|
||||
|
- 1XX 提示信息 - 表示请求已被成功接收,继续处理 |
||||
|
- 2XX 成功 - 表示请求已被成功接收,理解,接受 |
||||
|
- 3XX 重定向 - 要完成请求必须进行更进一步的处理 |
||||
|
- 4XX 客户端错误 - 请求有语法错误或请求无法实现 |
||||
|
- 5XX 服务器端错误 - 服务器未能实现合法的请求 |
||||
|
|
||||
|
我们看下面这个图展示了详细的返回信息,左边可以看到有很多的资源返回码,200是常用的,表示正常信息,302表示跳转。response header里面展示了详细的信息。 |
||||
|
|
||||
|
![](images/3.1.response.png?raw=true) |
||||
|
|
||||
|
图3.6 访问一次网站的全部请求信息 |
||||
|
|
||||
|
### HTTP协议是无状态的和Connection: keep-alive的区别 |
||||
|
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。从另一方面讲,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。 |
||||
|
|
||||
|
HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(面对无连接)。 |
||||
|
|
||||
|
从HTTP/1.1起,默认都开启了Keep-Alive保持连接特性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的TCP连接。 |
||||
|
|
||||
|
Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同服务器软件(如Apache)中设置这个时间。 |
||||
|
|
||||
|
## 请求实例 |
||||
|
|
||||
|
![](images/3.1.web.png?raw=true) |
||||
|
|
||||
|
图3.7 一次请求的request和response |
||||
|
|
||||
|
上面这张图我们可以了解到整个的通讯过程,同时细心的读者是否注意到了一点,一个URL请求但是左边栏里面为什么会有那么多的资源请求(这些都是静态文件,go对于静态文件有专门的处理方式)。 |
||||
|
|
||||
|
这个就是浏览器的一个功能,第一次请求url,服务器端返回的是html页面,然后浏览器开始渲染HTML:当解析到HTML DOM里面的图片连接,css脚本和js脚本的链接,浏览器就会自动发起一个请求静态资源的HTTP请求,获取相对应的静态资源,然后浏览器就会渲染出来,最终将所有资源整合、渲染,完整展现在我们面前的屏幕上。 |
||||
|
|
||||
|
>网页优化方面有一项措施是减少HTTP请求次数,就是把尽量多的css和js资源合并在一起,目的是尽量减少网页请求静态资源的次数,提高网页加载速度,同时减缓服务器的压力。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [Web基础](<03.0.md>) |
||||
|
* 下一节: [Go搭建一个Web服务器](<03.2.md>) |
@ -0,0 +1,25 @@ |
|||||
|
# 4 表单 |
||||
|
|
||||
|
表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉,但是对于C/C++程序员来说,这可能是一个有些陌生的东西,那么什么是表单呢? |
||||
|
|
||||
|
表单是一个包含表单元素的区域。表单元素(比如:文本域、下拉列表、单选框、复选框等等)是允许用户在表单中输入信息的元素。表单使用表单标签(\<form\>)定义。 |
||||
|
|
||||
|
<form> |
||||
|
... |
||||
|
input 元素 |
||||
|
... |
||||
|
</form> |
||||
|
|
||||
|
Go里面对于form处理已经有很方便的方法了,在Request里面有专门的form处理,可以很方便的整合到Web开发里面来,4.1小节里面将讲解Go如何处理表单的输入。由于不能信任任何用户的输入,所以我们需要对这些输入进行有效性验证,4.2小节将就如何进行一些普通的验证进行详细的演示。 |
||||
|
|
||||
|
HTTP协议是一种无状态的协议,那么如何才能辨别是否是同一个用户呢?同时又如何保证一个表单不出现多次递交的情况呢?4.3和4.4小节里面将对cookie(cookie是存储在客户端的信息,能够每次通过header和服务器进行交互的数据)等进行详细讲解。 |
||||
|
|
||||
|
表单还有一个很大的功能就是能够上传文件,那么Go是如何处理文件上传的呢?针对大文件上传我们如何有效的处理呢?4.5小节我们将一起学习Go处理文件上传的知识。 |
||||
|
|
||||
|
## 目录 |
||||
|
![](images/navi4.png?raw=true) |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [第三章总结](<03.5.md>) |
||||
|
* 下一节: [处理表单的输入](<04.1.md>) |
@ -0,0 +1,112 @@ |
|||||
|
# 4.1 处理表单的输入 |
||||
|
|
||||
|
先来看一个表单递交的例子,我们有如下的表单内容,命名成文件login.gtpl(放入当前新建项目的目录里面) |
||||
|
```html |
||||
|
|
||||
|
<html> |
||||
|
<head> |
||||
|
<title></title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<form action="/login" method="post"> |
||||
|
用户名:<input type="text" name="username"> |
||||
|
密码:<input type="password" name="password"> |
||||
|
<input type="submit" value="登录"> |
||||
|
</form> |
||||
|
</body> |
||||
|
</html> |
||||
|
``` |
||||
|
上面递交表单到服务器的`/login`,当用户输入信息点击登录之后,会跳转到服务器的路由`login`里面,我们首先要判断这个是什么方式传递过来,POST还是GET呢? |
||||
|
|
||||
|
http包里面有一个很简单的方式就可以获取,我们在前面web的例子的基础上来看看怎么处理login页面的form数据 |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"html/template" |
||||
|
"log" |
||||
|
"net/http" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
func sayhelloName(w http.ResponseWriter, r *http.Request) { |
||||
|
r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body) |
||||
|
//注意:如果没有调用ParseForm方法,下面无法获取表单的数据 |
||||
|
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 |
||||
|
fmt.Println("path", r.URL.Path) |
||||
|
fmt.Println("scheme", r.URL.Scheme) |
||||
|
fmt.Println(r.Form["url_long"]) |
||||
|
for k, v := range r.Form { |
||||
|
fmt.Println("key:", k) |
||||
|
fmt.Println("val:", strings.Join(v, "")) |
||||
|
} |
||||
|
fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的 |
||||
|
} |
||||
|
|
||||
|
func login(w http.ResponseWriter, r *http.Request) { |
||||
|
fmt.Println("method:", r.Method) //获取请求的方法 |
||||
|
if r.Method == "GET" { |
||||
|
t, _ := template.ParseFiles("login.gtpl") |
||||
|
log.Println(t.Execute(w, nil)) |
||||
|
} else { |
||||
|
//请求的是登录数据,那么执行登录的逻辑判断 |
||||
|
fmt.Println("username:", r.Form["username"]) |
||||
|
fmt.Println("password:", r.Form["password"]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
http.HandleFunc("/", sayhelloName) //设置访问的路由 |
||||
|
http.HandleFunc("/login", login) //设置访问的路由 |
||||
|
err := http.ListenAndServe(":9090", nil) //设置监听的端口 |
||||
|
if err != nil { |
||||
|
log.Fatal("ListenAndServe: ", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
通过上面的代码我们可以看出获取请求方法是通过`r.Method`来完成的,这是个字符串类型的变量,返回GET, POST, PUT等method信息。 |
||||
|
|
||||
|
login函数中我们根据`r.Method`来判断是显示登录界面还是处理登录逻辑。当GET方式请求时显示登录界面,其他方式请求时则处理登录逻辑,如查询数据库、验证登录信息等。 |
||||
|
|
||||
|
当我们在浏览器里面打开`http://127.0.0.1:9090/login`的时候,出现如下界面 |
||||
|
|
||||
|
![](images/4.1.login.png?raw=true) |
||||
|
|
||||
|
如果你看到一个空页面,可能是你写的 login.gtpl 文件中有错误,请根据控制台中的日志进行修复。 |
||||
|
|
||||
|
图4.1 用户登录界面 |
||||
|
|
||||
|
我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,Handler里面是不会自动解析form的,必须显式的调用`r.ParseForm()`后,你才能对这个表单数据进行操作。我们修改一下代码,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新编译,再次测试输入递交,现在是不是在服务器端有输出你的输入的用户名和密码了。 |
||||
|
|
||||
|
`r.Form`里面包含了所有请求的参数,比如URL中query-string、POST的数据、PUT的数据,所以当你在URL中的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。 |
||||
|
|
||||
|
现在我们修改一下login.gtpl里面form的action值`http://127.0.0.1:9090/login`修改为`http://127.0.0.1:9090/login?username=astaxie`,再次测试,服务器的输出username是不是一个slice。服务器端的输出如下: |
||||
|
|
||||
|
![](images/4.1.slice.png?raw=true) |
||||
|
|
||||
|
图4.2 服务器端打印接收到的信息 |
||||
|
|
||||
|
`request.Form`是一个url.Values类型,里面存储的是对应的类似`key=value`的信息,下面展示了可以对form数据进行的一些操作: |
||||
|
```Go |
||||
|
|
||||
|
v := url.Values{} |
||||
|
v.Set("name", "Ava") |
||||
|
v.Add("friend", "Jess") |
||||
|
v.Add("friend", "Sarah") |
||||
|
v.Add("friend", "Zoe") |
||||
|
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe" |
||||
|
fmt.Println(v.Get("name")) |
||||
|
fmt.Println(v.Get("friend")) |
||||
|
fmt.Println(v["friend"]) |
||||
|
|
||||
|
``` |
||||
|
>**Tips**: |
||||
|
>Request本身也提供了FormValue()函数来获取用户提交的参数。如r.Form["username"]也可写成r.FormValue("username")。调用r.FormValue时会自动调用r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [表单](<04.0.md>) |
||||
|
* 下一节: [验证表单的输入](<04.2.md>) |
@ -0,0 +1,72 @@ |
|||||
|
# 4.3 预防跨站脚本 |
||||
|
|
||||
|
现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多。所谓动态内容,就是根据用户环境和需要,Web应用程序能够输出相应的内容。动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 |
||||
|
|
||||
|
攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户信息,修改用户设置,盗取/污染cookie和植入恶意广告等。 |
||||
|
|
||||
|
对XSS最佳的防护应该结合以下两种方法:一是验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);另一个是对所有输出数据进行适当的处理,以防止任何已成功注入的脚本在浏览器端运行。 |
||||
|
|
||||
|
那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转义 |
||||
|
|
||||
|
- func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w |
||||
|
- func HTMLEscapeString(s string) string //转义s之后返回结果字符串 |
||||
|
- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串 |
||||
|
|
||||
|
|
||||
|
我们看4.1小节的例子 |
||||
|
```Go |
||||
|
|
||||
|
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端 |
||||
|
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) |
||||
|
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 |
||||
|
``` |
||||
|
如果我们输入的username是`<script>alert()</script>`,那么我们可以在浏览器上面看到输出如下所示: |
||||
|
|
||||
|
![](images/4.3.escape.png?raw=true) |
||||
|
|
||||
|
图4.3 Javascript过滤之后的输出 |
||||
|
|
||||
|
Go的html/template包默认帮你过滤了html标签,但是有时候你只想要输出这个`<script>alert()</script>`看起来正常的信息,该怎么处理?请使用text/template。请看下面的例子: |
||||
|
```Go |
||||
|
|
||||
|
import "text/template" |
||||
|
... |
||||
|
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) |
||||
|
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>") |
||||
|
``` |
||||
|
输出 |
||||
|
|
||||
|
Hello, <script>alert('you have been pwned')</script>! |
||||
|
|
||||
|
或者使用template.HTML类型 |
||||
|
```Go |
||||
|
|
||||
|
import "html/template" |
||||
|
... |
||||
|
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) |
||||
|
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>")) |
||||
|
``` |
||||
|
输出 |
||||
|
|
||||
|
Hello, <script>alert('you have been pwned')</script>! |
||||
|
|
||||
|
转换成`template.HTML`后,变量的内容也不会被转义 |
||||
|
|
||||
|
转义的例子: |
||||
|
```Go |
||||
|
|
||||
|
import "html/template" |
||||
|
... |
||||
|
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) |
||||
|
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>") |
||||
|
``` |
||||
|
转义之后的输出: |
||||
|
|
||||
|
Hello, <script>alert('you have been pwned')</script>! |
||||
|
|
||||
|
|
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [验证的输入](<04.2.md>) |
||||
|
* 下一节: [防止多次递交表单](<04.4.md>) |
@ -0,0 +1,60 @@ |
|||||
|
# 4.4 防止多次递交表单 |
||||
|
|
||||
|
不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢? |
||||
|
|
||||
|
解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该唯一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。 |
||||
|
|
||||
|
我继续拿4.2小节的例子优化: |
||||
|
```html |
||||
|
|
||||
|
<input type="checkbox" name="interest" value="football">足球 |
||||
|
<input type="checkbox" name="interest" value="basketball">篮球 |
||||
|
<input type="checkbox" name="interest" value="tennis">网球 |
||||
|
用户名:<input type="text" name="username"> |
||||
|
密码:<input type="password" name="password"> |
||||
|
<input type="hidden" name="token" value="{{.}}"> |
||||
|
<input type="submit" value="登陆"> |
||||
|
``` |
||||
|
我们在模版里面增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取唯一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。 |
||||
|
```Go |
||||
|
|
||||
|
func login(w http.ResponseWriter, r *http.Request) { |
||||
|
fmt.Println("method:", r.Method) //获取请求的方法 |
||||
|
if r.Method == "GET" { |
||||
|
timestamp := strconv.Itoa(time.Now().Nanosecond()) |
||||
|
hashWr := md5.New() |
||||
|
hashWr.Write([]byte(timestamp)) |
||||
|
token := fmt.Sprintf("%x", hashWr.Sum(nil)) |
||||
|
|
||||
|
t, _ := template.ParseFiles("login.gtpl") |
||||
|
t.Execute(w, token) |
||||
|
} else { |
||||
|
//请求的是登陆数据,那么执行登陆的逻辑判断 |
||||
|
r.ParseForm() |
||||
|
token := r.Form.Get("token") |
||||
|
if token != "" { |
||||
|
//验证token的合法性 |
||||
|
} else { |
||||
|
//不存在token报错 |
||||
|
} |
||||
|
fmt.Println("username length:", len(r.Form["username"][0])) |
||||
|
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端 |
||||
|
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) |
||||
|
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
上面的代码输出到页面的源码如下: |
||||
|
|
||||
|
![](images/4.4.token.png?raw=true) |
||||
|
|
||||
|
图4.4 增加token之后在客户端输出的源码信息 |
||||
|
|
||||
|
我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。 |
||||
|
|
||||
|
我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [预防跨站脚本](<04.3.md>) |
||||
|
* 下一节: [处理文件上传](<04.5.md>) |
@ -0,0 +1,161 @@ |
|||||
|
# 4.5 处理文件上传 |
||||
|
你想处理一个由用户上传的文件,比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢? |
||||
|
|
||||
|
要使表单能够上传文件,首先第一步就是要添加form的`enctype`属性,`enctype`属性有如下三种情况: |
||||
|
``` |
||||
|
|
||||
|
application/x-www-form-urlencoded 表示在发送前编码所有字符(默认) |
||||
|
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。 |
||||
|
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。 |
||||
|
``` |
||||
|
所以,创建新的表单html文件, 命名为upload.gtpl, html代码应该类似于: |
||||
|
```html |
||||
|
|
||||
|
<html> |
||||
|
<head> |
||||
|
<title>上传文件</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<form enctype="multipart/form-data" action="/upload" method="post"> |
||||
|
<input type="file" name="uploadfile" /> |
||||
|
<input type="hidden" name="token" value="{{.}}"/> |
||||
|
<input type="submit" value="upload" /> |
||||
|
</form> |
||||
|
</body> |
||||
|
</html> |
||||
|
``` |
||||
|
在服务器端,我们增加一个handlerFunc: |
||||
|
```Go |
||||
|
|
||||
|
http.HandleFunc("/upload", upload) |
||||
|
|
||||
|
// 处理/upload 逻辑 |
||||
|
func upload(w http.ResponseWriter, r *http.Request) { |
||||
|
fmt.Println("method:", r.Method) //获取请求的方法 |
||||
|
if r.Method == "GET" { |
||||
|
crutime := time.Now().Unix() |
||||
|
h := md5.New() |
||||
|
io.WriteString(h, strconv.FormatInt(crutime, 10)) |
||||
|
token := fmt.Sprintf("%x", h.Sum(nil)) |
||||
|
|
||||
|
t, _ := template.ParseFiles("upload.gtpl") |
||||
|
t.Execute(w, token) |
||||
|
} else { |
||||
|
r.ParseMultipartForm(32 << 20) |
||||
|
file, handler, err := r.FormFile("uploadfile") |
||||
|
if err != nil { |
||||
|
fmt.Println(err) |
||||
|
return |
||||
|
} |
||||
|
defer file.Close() |
||||
|
fmt.Fprintf(w, "%v", handler.Header) |
||||
|
f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) // 此处假设当前目录下已存在test目录 |
||||
|
if err != nil { |
||||
|
fmt.Println(err) |
||||
|
return |
||||
|
} |
||||
|
defer f.Close() |
||||
|
io.Copy(f, file) |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
通过上面的代码可以看到,处理文件上传我们需要调用`r.ParseMultipartForm`,里面的参数表示`maxMemory`,调用`ParseMultipartForm`之后,上传的文件存储在`maxMemory`大小的内存里面,如果文件大小超过了`maxMemory`,那么剩下的部分将存储在系统的临时文件中。我们可以通过`r.FormFile`获取上面的文件句柄,然后实例中使用了`io.Copy`来存储文件。 |
||||
|
|
||||
|
>获取其他非文件字段信息的时候就不需要调用`r.ParseForm`,因为在需要的时候Go自动会去调用。而且`ParseMultipartForm`调用一次之后,后面再次调用不会再有效果。 |
||||
|
|
||||
|
通过上面的实例我们可以看到我们上传文件主要三步处理: |
||||
|
|
||||
|
1. 表单中增加enctype="multipart/form-data" |
||||
|
2. 服务端调用`r.ParseMultipartForm`,把上传的文件存储在内存和临时文件中 |
||||
|
3. 使用`r.FormFile`获取文件句柄,然后对文件进行存储等处理。 |
||||
|
|
||||
|
文件handler是multipart.FileHeader,里面存储了如下结构信息 |
||||
|
```Go |
||||
|
|
||||
|
type FileHeader struct { |
||||
|
Filename string |
||||
|
Header textproto.MIMEHeader |
||||
|
// contains filtered or unexported fields |
||||
|
} |
||||
|
``` |
||||
|
我们通过上面的实例代码打印出来上传文件的信息如下 |
||||
|
|
||||
|
![](images/4.5.upload2.png?raw=true) |
||||
|
|
||||
|
图4.5 打印文件上传后服务器端接受的信息 |
||||
|
|
||||
|
## 客户端上传文件 |
||||
|
|
||||
|
我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实Go支持模拟客户端表单功能支持文件上传,详细用法请看如下示例: |
||||
|
```Go |
||||
|
|
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"io/ioutil" |
||||
|
"mime/multipart" |
||||
|
"net/http" |
||||
|
"os" |
||||
|
) |
||||
|
|
||||
|
func postFile(filename string, targetUrl string) error { |
||||
|
bodyBuf := &bytes.Buffer{} |
||||
|
bodyWriter := multipart.NewWriter(bodyBuf) |
||||
|
|
||||
|
//关键的一步操作 |
||||
|
fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename) |
||||
|
if err != nil { |
||||
|
fmt.Println("error writing to buffer") |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
//打开文件句柄操作 |
||||
|
fh, err := os.Open(filename) |
||||
|
if err != nil { |
||||
|
fmt.Println("error opening file") |
||||
|
return err |
||||
|
} |
||||
|
defer fh.Close() |
||||
|
|
||||
|
//iocopy |
||||
|
_, err = io.Copy(fileWriter, fh) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
contentType := bodyWriter.FormDataContentType() |
||||
|
bodyWriter.Close() |
||||
|
|
||||
|
resp, err := http.Post(targetUrl, contentType, bodyBuf) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
defer resp.Body.Close() |
||||
|
resp_body, err := ioutil.ReadAll(resp.Body) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
fmt.Println(resp.Status) |
||||
|
fmt.Println(string(resp_body)) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// sample usage |
||||
|
func main() { |
||||
|
target_url := "http://localhost:9090/upload" |
||||
|
filename := "./astaxie.pdf" |
||||
|
postFile(filename, target_url) |
||||
|
} |
||||
|
|
||||
|
``` |
||||
|
上面的例子详细展示了客户端如何向服务器上传一个文件的例子,客户端通过multipart.Write把文件的文本流写入一个缓存中,然后调用http的Post方法把缓存传到服务器。 |
||||
|
|
||||
|
>如果你还有其他普通字段例如username之类的需要同时写入,那么可以调用multipart的WriteField方法写很多其他类似的字段。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [防止多次递交表单](<04.4.md>) |
||||
|
* 下一节: [小结](<04.6.md>) |
@ -0,0 +1,9 @@ |
|||||
|
# 4.6 小结 |
||||
|
这一章里面我们学习了Go如何处理表单信息,我们通过用户登录、上传文件的例子展示了Go处理form表单信息及上传文件的手段。但是在处理表单过程中我们需要验证用户输入的信息,考虑到网站安全的重要性,数据过滤就显得相当重要了,因此后面的章节中专门写了一个小节来讲解了不同方面的数据过滤,顺带讲一下Go对字符串的正则处理。 |
||||
|
|
||||
|
通过这一章能够让你了解客户端和服务器端是如何进行数据上的交互,客户端将数据传递给服务器系统,服务器接受数据又把处理结果反馈给客户端。 |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一节: [处理文件上传](<04.5.md>) |
||||
|
* 下一章: [访问数据库](<05.0.md>) |
@ -0,0 +1,16 @@ |
|||||
|
# 5 访问数据库 |
||||
|
对许多Web应用程序而言,数据库都是其核心所在。数据库几乎可以用来存储你想查询和修改的任何信息,比如用户信息、产品目录或者新闻列表等。 |
||||
|
|
||||
|
Go没有内置的驱动支持任何的数据库,但是Go定义了database/sql接口,用户可以基于驱动接口开发相应数据库的驱动,5.1小节里面介绍Go设计的一些驱动,介绍Go是如何设计数据库驱动接口的。5.2至5.4小节介绍目前使用的比较多的一些关系型数据驱动以及如何使用,5.5小节介绍我自己开发一个ORM库,基于database/sql标准接口开发的,可以兼容几乎所有支持database/sql的数据库驱动,可以方便的使用Go style来进行数据库操作。 |
||||
|
|
||||
|
目前NOSQL已经成为Web开发的一个潮流,很多应用采用了NOSQL作为数据库,而不是以前的缓存,5.6小节将介绍MongoDB和Redis两种NOSQL数据库。 |
||||
|
|
||||
|
>[Go database/sql tutorial](http://go-database-sql.org/) 里提供了惯用的范例及详细的说明。 |
||||
|
|
||||
|
## 目录 |
||||
|
![](images/navi5.png?raw=true) |
||||
|
|
||||
|
## links |
||||
|
* [目录](<preface.md>) |
||||
|
* 上一章: [第四章总结](<04.6.md>) |
||||
|
* 下一节: [database/sql接口](<05.1.md>) |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 190 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 361 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,78 @@ |
|||||
|
package main |
||||
|
|
||||
|
/** |
||||
|
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下: |
||||
|
|
||||
|
type struct_variable_type struct { |
||||
|
member definition |
||||
|
member definition |
||||
|
... |
||||
|
member definition |
||||
|
} |
||||
|
一旦定义了结构体类型,它就能用于变量的声明,语法格式如下: |
||||
|
|
||||
|
variable_name := structure_variable_type {value1, value2...valuen} |
||||
|
或 |
||||
|
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen} |
||||
|
|
||||
|
访问结构体成员 |
||||
|
如果要访问结构体成员,需要使用点号 . 操作符,格式为: |
||||
|
|
||||
|
结构体.成员名" |
||||
|
|
||||
|
*/ |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
type A01 struct { |
||||
|
a0000 string |
||||
|
a0101 string |
||||
|
a0104 string |
||||
|
a0107 string |
||||
|
a0184 int |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
fmt.Println("\n ================== 结构体 方式一 = 开始 ================== ") |
||||
|
|
||||
|
zhangs := A01{"AASUHASASY56UA466", "张三", "男", "19920817", 130434199208173919} |
||||
|
fmt.Println(zhangs) |
||||
|
|
||||
|
lis := A01{a0000: "LISI7812244AS47", a0101: "李四", a0104: "男", a0107: "87878787", a0184: 132456878787878787} |
||||
|
fmt.Println(lis) |
||||
|
|
||||
|
wangw := A01{a0000: "WANGWU744AS47", a0101: "王五", a0104: "男"} |
||||
|
fmt.Println(wangw) |
||||
|
|
||||
|
var mal A01 |
||||
|
mal.a0000 = "MALIU7982235585" |
||||
|
mal.a0101 = "马六" |
||||
|
mal.a0104 = "女" |
||||
|
mal.a0107 = "19900306" |
||||
|
mal.a0184 = 110235199003061234 |
||||
|
fmt.Println(mal) |
||||
|
|
||||
|
//传递对象 -> 获取的对象
|
||||
|
printA01(mal) |
||||
|
fmt.Println("方法定义指针参数,调用传参是取地址,这样获取就是对象") |
||||
|
|
||||
|
//传递地址 -> 获取为地址,使用指针即可获取对象[地址 -> 指针 || &对象 => *对象]
|
||||
|
printA01ZZ(&mal) |
||||
|
} |
||||
|
|
||||
|
/* 打印对象*/ |
||||
|
func printA01(a01 A01) { |
||||
|
fmt.Printf("人员%s的唯一编码为:%s\n", a01.a0101, a01.a0000) |
||||
|
fmt.Printf("人员%s的性别为:%s\n", a01.a0101, a01.a0104) |
||||
|
fmt.Printf("人员%s的出生年月为:%s\n", a01.a0101, a01.a0107) |
||||
|
fmt.Printf("人员%s的身份证号为:%d\n", a01.a0101, a01.a0184) |
||||
|
} |
||||
|
|
||||
|
/* 打印结构体指针 */ |
||||
|
func printA01ZZ(a01 *A01) { |
||||
|
fmt.Printf("唯一编码为%s\n", a01.a0000) |
||||
|
fmt.Printf("姓名为:%s\n", a01.a0101) |
||||
|
fmt.Printf("性别为:%s\n", a01.a0104) |
||||
|
fmt.Printf("出生年月为:%s\n", a01.a0107) |
||||
|
fmt.Printf("身份证号为:%d\n", a01.a0184) |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
package main |
||||
|
|
||||
|
/** |
||||
|
if 语句 if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 |
||||
|
if...else 语句 if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 |
||||
|
if 嵌套语句 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。 |
||||
|
switch 语句 switch 语句用于基于不同条件执行不同动作。 |
||||
|
select 语句 select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。 |
||||
|
|
||||
|
关于 switch 的 fallthrough |
||||
|
使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。 |
||||
|
|
||||
|
|
||||
|
*/ |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
func main() { |
||||
|
var x interface{} |
||||
|
|
||||
|
switch i := x.(type) { |
||||
|
case nil: |
||||
|
fmt.Printf(" x 的类型 :%T", i) |
||||
|
case int: |
||||
|
fmt.Printf("x 是 int 型") |
||||
|
case float64: |
||||
|
fmt.Printf("x 是 float64 型") |
||||
|
case func(int) float64: |
||||
|
fmt.Printf("x 是 func(int) 型") |
||||
|
case bool, string: |
||||
|
fmt.Printf("x 是 bool 或 string 型") |
||||
|
default: |
||||
|
fmt.Printf("未知型") |
||||
|
} |
||||
|
} |