Go语言极限入门
# Go语言极限入门
参考书目: 《Go程序设计语言》
# 快速入门
如下是hello world程序:
// hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}
2
3
4
5
6
7
8
终端执行 go run hello.go
。
Go代码是用包来组织的,包类似于其他语言中的库和模块。
package main
指明了这个文件属于哪个包。
后面跟着导入的是其他包的列表,fmt用于格式化输出和扫描输入。
main包比较特殊,它用来定义一个独立的可执行程序,而不是库。import声明必须跟在package声明之后。import导入声明后,是组成程序的函数。
一个函数的声明由func关键字、函数名、参数列表(main函数为空)、返回值列表和函数体构成。
# 命令行参数
命令行参数以os包中Args名字的变量供程序访问,在os包外面,使用os.Args这个名字,这是一个字符串slice。
// echo.go 输出命令行参数
package main
import (
"fmt"
"os"
)
func main() {
var s, sep string
for i := 1; i < len(os.Args); i++ {
s += sep + os.Args[i]
sep = " "
}
fmt.Println(s)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ go build echo.go
./echo hello
hello
2
3
var 关键字声明了两个string类型的变量s和sep。变量可以声明的时候初始化。如果变量没有明确地初始化,它将隐式初始化这个类型的空值。
for 是 go里面唯一的循环语句。
for initlization; condition; post {
//语句
}
2
3
可选的initialization(初始化)语句在循环开始之前执行。如果存在,它必须是一个简单的语句。三部分都是可省的,如果三部分都不存在,只有一个for,那就是无限循环。
另一种形式的for循环是在字符串或slice数据上迭代。
如下是第二种echo程序:
// echo.go
package main
import (
"fmt"
"os"
)
func main() {
var s, sep string
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
每一次迭代,range都产生一对值: 索引和这个索引处元素的值。因为这个例子里用不到索引,但是语法上range循环需要处理。应次也必须处理索引。可以将索引赋予一个临时变量,然后忽略它,但是go不允许存在无用的变量。选择使用空标识符"__"。空标识符可以用在任何语法需要变量名但逻辑不需要的地方。
如果有大量的数据要处理,这样做的代价会比较大。可以使用strings包中的Join
函数。
package main
import (
"fmt"
"os"
"strings"
)
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
2
3
4
5
6
7
8
9
10
11
# 找出重复行
如下程序要输出标准输入中出现次数大于1的行,前面是次数。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
counts[input.Text()]++
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在上述这个程序中,引入了if语句、map类型和bufio包。
像for一样,if语句中的条件部分也从不放在圆括号里。
map存储一个键值对集合。在这里map的键是字符串,值是数字。内置的函数make可以用来新建map,它还可以有其他用途。
counts := make(map[string]int)
每次从输入读取一行内容,这一行就作为map中的键,对应的值递增1。键在map中不存在时也是没有问题的。为了输出结果,使用基于range的for循环。
bufio包,使用它可以简便和高效地处理输入和输出。其中一个最有用的特性是称为扫描器(Scanner)的类型,可以读取输入,以行或者单词为单位断开。
input := bufio.NewScanner(os.Stdin)
Printf函数有超过10个转义字符:
verb | 描述 |
---|---|
%d | 十进制整数 |
%x,%o,%b | 十六进制、八进制、二进制整数 |
%f,%g,%e | 浮点数 |
%t | 布尔类型 |
%c | 字符 |
%s | 字符串 |
%q | 带引号字符串 |
%v | 内置格式的任何值 |
%T | 任何值的类型 |
%% | 百分号本身 |
如下是从文件中读取字符串:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup: %v\n", err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
func countLines(f *os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
读取的文件如下:
$ cat test.txt
AAAAAAAA
BBBBBBB
AAAAAAAA
CCCCCCC
HHHHHH
2
3
4
5
6
输入如下:
$ ./main test.txt
2 AAAAAAAA
2
上述程序是采用"流式"模式读取输入,然后按需拆分为行。
这里引入一个ReadFile函数(从io/ioutil包导入),它读取整个命名文件的内容,还引入一个strings.Split函数,将一个字符串分割为一个由子串组成的slice:
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
func main() {
counts := make(map[string]int)
for _,filename := range os.Args[1:] {
data,err := ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr,"dup: %v\n",err)
continue
}
for _,line := range strings.Split(string(data),"\n") {
counts[line]++
}
}
for line,n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n",n,line)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ReadFile函数返回一个可以转化成字符串的字节slice,这样它可以被strings.Split分割。
# 获取一个URL
Go提供了一系列包,在net包下面分组管理,使用它们可以方便地通过互联网发送和接受信息。
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
for _,url := range os.Args[1:] {
resp,err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr,"fetch: %v\n",err)
os.Exit(1)
}
b,err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr,"fetch: reading %s: %v\n",url,err)
os.Exit(1)
}
fmt.Printf("%s",b)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
程序fetch展示从互联网获取信息的最小需求,它获取每个指定URL的内容,然后不加解析地输出。fetch来自curl工具。
这个程序使用的函数来自两个包: net/http和io/ioutil。http.Get函数产生一个HTTP请求,如果没有出错,返回结果存在响应结构resp里面,其中resp的Body域包含服务器端响应的一个可读取数据流。随后ioutil.ReadAll读取整个响应结果并存入b。
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
fmt.Printf("%s\n", b)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
关闭Body数据流来避免资源泄露。
运行结果:
$ ./fetch https://www.baidu.com
<html>
<head>
<script>
location.replace(location.href.replace("https://","http://"));
</script>
</head>
<body>
<noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
也可将程序改写:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
_, err = fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
for {
written, err := io.Copy(os.Stdout, resp.Body)
if written == 0 {
break
}
if err != nil {
_, err = fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 并发获取多个URL
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"time"
)
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch) // 启动一个goroutine
}
for range os.Args[1:] {
fmt.Println(<-ch) // 从通道ch接收
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}
func fetch(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err)
return
}
nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close() // 防止泄露资源
if err != nil {
ch <- fmt.Sprintf("while reading %s: %v", url, err)
return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
运行结果:
$ ./fetchall http://www.baidu.com http://www.qq.com
0.08s 352723 http://www.baidu.com
0.14s 173953 http://www.qq.com
0.14s elapsed
2
3
4
这个进程可以并发获取很多URL内容,于是这个进程使用的时间不超过耗时最长时间的任务。这个程序不保存响应内容,但会报告每个响应的大小和花费的时间。
gorotine是一个并发执行的函数。通道是一种允许某一进程向另一种进程传递制定类型的值的通信机制。main函数在一个goroutine中执行,然后go语句创建额外的goroutine。
main函数使用make创建一 个字符串通道。对于每个命令行参数,go语句在第一轮循环中启动一个新的goroutine,它异步调用fetch来使用http.Get获取URL内容。io.Copy函数读取响应的内容,然后通过写入ioutil.Discard输出流进行丢弃。Copy返回字节数和错误信息。每一个结果返回时,fetch发送一行汇总信息到通道ch。main中第二轮循环接收并且输出那些汇总行。
# 一个WEB服务器
如下代码,实现一个简单的服务器,将返回服务器URL路径部分:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
运行结果:
$ ./fetch http://localhost:8000/help
URL.Path = "/help"
2
这里的库函数做了大部分工作。main函数将一个处理函数和以/开头的URL链接在一起,代表所有的URL使用这个函数处理,然后启动服务器监听8000端口处的请求。一个请求由http.Request类型的结构体表示,它包含很多关联的域,其中一个是所请求的URL。当一个请求到达时,它被转交给处理函数,并从请求的URL中提取路径部分,使用fmt.Printf格式化,然后作为响应发送回去。
为服务器添加功能也很简单,如下程序会返回收到的请求数量:
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
// 回显目前为止调用的次数
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
运行结果:
$ ./fetch http://localhost:8000/
URL.Path = "/"
$ ./fetch http://localhost:8000/
URL.Path = "/"
$ ./fetch http://localhost:8000/count
Count 2
2
3
4
5
6
这个服务器有两个处理函数,通过请求的URL来决定哪一个被调用: 请求/count调用counter,其他的调用handler。
以/结尾的处理模式匹配所有含有这个前缀的URL。在后台,对于每个传入的请求,服务器在不同的goroutine中运行该处理函数,这样它可以同时处理多个请求。
然而,如果两个并发的请求试图同时更新计数值count,count可能会不一致地增加,程序会产生一个严重的竞态BUG。为了避免该问题,必须确保最多只有一个goroutine在同一时间访问变量,这正是mu.Lock()和mu.Unlock()语句的作用。
修改处理函数,使其可以报告接收到的消息头和表单数据,这样可以方便服务器审查和调试请求。
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
运行结果:
$ ./fetch http://localhost:8000/
GET / HTTP/1.1
Header["User-Agent"] = ["Go-http-client/1.1"]
Header["Accept-Encoding"] = ["gzip"]
Host = "localhost:8000"
RemoteAddr = "127.0.0.1:47766"
2
3
4
5
6
# 程序结构
声明是给一个程序实体命名,并且设定其部分或全部属性。有4个主要声明: 变量(var)、常量(const)、类型(type)函数(func)。
Go程序存储在一个或多个以.go为后缀的文件里。每一个文件以package声明开头,表明文件属于哪个包。package 声明后面是import声明,然后是包级别的类型、变量、常量、函数的声明,不区分顺序。
例如,下面的程序声明一个常量、一个函数和一对变量:
// 输出水的沸点
package main
import "fmt"
const boilingF = 212.0
func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %g F or %g C\n", f, c)
}
// 输出: boiling point = 212 F or 100 C
2
3
4
5
6
7
8
9
10
11
12
13
14
常量boilingF是一个包级别的声明(main包),f和c是属于main函数的局部变量。包级别的实体名字不仅对于包含其声明的源文件可见,而且对于同一个包里面的所有源文件可见。
另一方面,局部声明仅仅是在声明所在的函数内部可见,并且可能对于函数中的一小块区域可见。
函数的声明包含一个名字、参数列表(由函数的调用者提供的变量)、一个可选的返回值列表,以及函数体。
下面的函数fToC封装了温度转换的逻辑,这样可以只定义一次而在多个地方使用。
package main
import "fmt"
func main() {
const freezingF, boilingF = 32.0, 212.0
fmt.Printf("%g F = %g C\n", freezingF, fToC(freezingF))
fmt.Printf("%g F = %g C\n", boilingF, fToC(boilingF))
}
func fToC(f float64) float64 {
return (f - 32) * 5 / 9
}
/* 输出:
32 F = 0 C
212 F = 100 C
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 变量
通用形式: var name type = expression
。
类型和表达式部分可以省略一个,但不能都省略。
如果类型省略,它的类型将由初始化表达式决定。如果表达式省略,其初始值对应于类型的零值,因此Go中不存在未初始化变量。
# 短变量声明
在函数中,一种称作短变量声明的可选形式可以用来初始化局部变量。
形式: name := expression
,name的类型由expression的类型来决定。
在局部变量的声明和初始化主要使用短声明。
var声明通常是为那些跟初始化表达式类型不一致的局部变量保留的,或者用于后面才对变量赋值以及变量初始值不重要的情况。
i := 100
var boiling float64 = 100
i,j := 0,1
2
3
# 指针
指针的值是一个变量的地址。
如果一个变量声明为var x int
,表达式&x获取一个指向整型变量的指针。
x := 1
p := &x // p 是整型指针 只想x
fmt.Println(*p) // "1"
*p = 2 // 等价于x = 2
fmt.Println(x) // 结果"2"
2
3
4
5
每个聚合类型变量的组成都是变量,所以也有一个地址。
指针类型的零值是nil。
函数可以返回局部变量的地址。
var p = f()
func f() *int {
v := 1
return &v
}
2
3
4
5
6
因为一个指针包含变量的地址,所以传递一个指针参数给函数,能够让函数更新间接传递的变量值。
func incr(p *int) int {
*p++ // 递增p所指向的值 p自身保持不变
return *p
}
v := 1
incr(&v) // v 等于 2
fmt.Println(incr(&v)) // "3"
2
3
4
5
6
7
8
指针对于flag包是很关键的,它使用程序的命令行参数来设置整个程序内某些变量的值。
package main
import (
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flag.Bool函数创建一个新的布尔标识变量,它有3个参数。变量sep和n是指向标识变量的指针,必须通过sep和n来访问。
当程序运行前,在使用标识前,必须调用flag.Parse来更新标识变量的默认值。非标识参数也可以从flag.Args()返回的字符串slice来访问。如果flag.Parse遇到错误,它输出一条帮助信息,然后调用os.Exit(2)来结束程序。
运行示例:
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -help
Usage of ./echo4:
-n omit trailing newline
-s string
separator (default " ")
2
3
4
5
6
7
8
9