Go语言学习(一)
开新坑,从黑砖学习
第1章 入门
这本书比上一本书感觉要难得多,还是觉得黑砖还是strong
,结合个人学习经历,还是先学上一本。
1.1 hello world
package main
import "fmt"
func main(){
fmt.Println("Hello,世界")
}
- 1.Go是编译型的语言。Go的工具链将程序的源文件转变为机器相关的原生二进制指令
- 2.Go原生地支持Unicode,所以他可以处理所有国家的语言
- 3.Go代码是使用包来组织的,包类似于其他语言中的库和模块。一个包由一个或多个.go源文件组成,放在一个文件夹中,该文件夹的名字描述了包的作用。
- 4.名为main的包比较特殊,它用来定义一个独立的可执行程序,而不是库。在main包中,函数main也是特殊的。不管在什么程序中,main做什么事情,他总是程序开始执行的地方。
- 5.GO不需要在函数或声明后面使用分号结尾,除非有多个语句或声明出现在同一行。
- 6."{"符号必须和关键字func在同一行,不能独自成行,并且在x+y这个表达式中,换行符可以在+操作符的后面,但是不能在+操作符的前面.
- 7.
goimports
可以按需管理导入声明的插入和移除。它不是标准发布版的一部分,可以通过执行下面的命令获取到:go get golang.org/x/tools/cmd/goimports
1.2 命令行参数
1.2.1 echo1
package main
import (//导入需要的包
"fmt"
"os"
)
func main(){
var s, sep string //定义两个字符串类型的变量
for i := 1; i < len(os.Args); i++ { //for循环命令行参数
s += sep + os.Args[i]
sep = " "
}
fmt.Println(s)
}
- 1.注释以
//
开头 - 2.变量未初始化,则默认为该类型的空值
- 3.
i++
是语句,不是表达式,所以j=i++
非法,而且Go只支持后缀,所以++i
非法 - 4.
os.Args
是命令行的内容,os.Arg[0]
是命令行的命令,后面的是参数
1.2.2 echo2
package main
import (
"fmt"
"os"
)
func main(){
s, sep := "",""
for _, arg := range os.Args[1:]{
s += sep + arg
sep = " "
}
fmt.Println(s)
}
- 1.每一次迭代,range产生一对值:索引和这个索引处元素的值
- 2.简短声明变量的使用要求是
:=
左侧存在未初始化的变量 - 4.使用空标识符,他的名字是
_(即下划线)
,空标识符可以用在任何语法需要变量名但是程序逻辑不需要的地方,例如丢弃每次迭代产生的无用的索引。大多数Go程序员喜欢搭配使用range
和_
来写上面的echo
程序,因为索引在os.Args
上面是隐式的,所以更不容易犯错。
1.2.3 echo3
package main
import (
"fmt"
"os"
"string"
)
func main(){
fmt.Println(strings.Join(os.Args[1:]," "))
}
- 1.如果数组庞大的时候使用
+=
将会效率低下,使用strings包中的Join函数
1.3 找出重复行
- 在这里,作者给了三个版本的
dup
程序,这个程序是受UNIX
的uniq
命令启发来找到的相邻的重复行。uniq
命令我也不清楚是什么,问度娘之后得知:uniq命令是UNIX/LINUX下一种文件内容去重的命令
1.3.1 dup程序1.0
这个版本输出标准输入中出现次数大于1的行,前面是次数。
//gop1.io/ch1/dup1
//dup1输出标准输入中出现次数大于1的行,前面是次数
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
count := make(map[string]int)//建立一个map类型的映射关系,string——>int
input := bufio.NewScanner(os.Stdin)//建立一个bufio.Scanner类型的input变量
for input.Scan() {
count[input.Text()]++
}
//注意:忽略input.Err()中可能的错误
for line, n := range count {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
- 1.
map[string]int
:定义了一个键为string类型,值为int型的map类型,map
储存一个键/值对集合,并且提供常量时间的操作来储存、获取或测试集合中的某个元素。键可以是其值能够进行相等(==)比较的任意类型,字符串是最常见的例子;值可以是任意类型。内置的make
函数可以用来新建map。
map中的键的迭代顺序不是固定的,通常是随机的,这是为了防止程序依赖某种特定的序列,emm,这tmua的不就是python中的dict么? - 2.每次dup从输入读取一行内容,这一行内容就作为map中的键,对应的值递增1。
语句counts[input.Text()]++等价于下面两个语句:
line:=input.Text()
counts[line]=counts[line]+1
这里的就很有意思了,这个类似于python对dict的get操作,dict.get(s),如果s不存在返回0
这里也是一样,如果line不存在counts中,则返回值相应类型的零值 - 3.使用
bufio
包可以简便和高效的处理输入和输出。其中一个最有用的特性是称为扫描器(Scanner)的类型,它可以读取输入,以行或单词为单位断开,这是处理以行为单位的输入内容的最简单的方式 - 4.每次调用input.Scan()读取下一行并将结尾的换行符去掉;通过调用input.Text()来读取读到的内容。Scan函数在读到新行的使用返回true,反之返回false
先输入要扫描的文本,然后使用Ctrl+C执行
- 5.使用fmt.Printf格式化输出
字符串字面量可以包含类似转义序列(escape sequence)来表示不可见字符。Printf默认不写换行符。按照约定,诸如log.Printtf和fmt.Errorf之类的格式化函数以f结尾,使用个fmt.Printf相同的格式化规则;而那些以ln结尾的函数(如Println)则使用
%v
的方式来格式化参数,并在最后追加换行符。
1.3.2 dup程序2.0
从文件中读取内容并判断重复行
package main
import (
"fmt"
"bufio"
"os"
)
func countLines(f *os.File,counts map[string]int){
input := bufio.NewScanner(f)
for input.Scan(){
counts[input.Text()]++
}
}
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,"dup2: %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)
}
}
}
函数os.Open返回两个值。
第一个是打开的文件(*os.File),该文件随后被Scanner读取。
第二个是内置的error类型的值。
- 简单的错误处理是使用Fprintln和%v在标准错误流上输出一条信息,%v可以使用默认格式显示任意类型的值;错误处理后,dup开始处理下一个文件;continue语句让循环进入下一个迭代。
- 这里忽略了input.Scan读取文件过程中的错误。
- 对countLines的调用出现在其声明之前。函数和其他包级别的实体可以以任何次序声明。
- 在函数countLines中的参数类型map,是一个使用make创建的数据结构的引用。当一个map被传递给一个函数时,函数接收到这个引用的副本,所以被调用函数中对于map数据结构中的改变对函数调用者使用的map引用也是可见的。在实例中,countLines函数在counts map中插入的值在main函数中也是可见的。
1.3.3 dup程序3.0
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,"dup3:%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)
}
}
}
这个版本相较于上一个版本来说,是一次性将所有的数据读入内存,上一个版本是采用流式处理方法
- ReadFile()函数(io/ioutil包)读取整个命名文件的内容
- strings.Split()函数,将一个字符串分割为一个由子串组成的slice。(Split是前面介绍过的strings.Join的反操作)
- 在dup3中我们进行了简化操作:第一,它仅读取指定的文件,而非标准输入,因为ReadFile需要一个文件名作为参数;第二,我们将统计行数的工作放回main函数中,因为它当前仅在一处用到。
1.4 GIF动画
//存在问题,实现失败
package main
import (
"image"
"image/color"
"image/gif"
"io"
"log"
"net/http"
"math"
"time"
"math/rand"
"os"
)
var palette = []color.Color{color.White, color.Black}
const (
whiteIndex = 0 //画板中的第一种颜色
blackIndex = 1 //画板中的第二种颜色
)
func main(){
rand.Seed(time.Now().UTC().UnixNano())
if len(os.Args) > 1 && os.Args[1] == "web" {
handler := func(w http.ResponseWriter, r *http.Request){
lissajous(w)
}
http.HandleFunc("/",handler)
log.Fatal(http.ListenAndServe("localhost:8000",nil))
return
}
lissajous(os.Stdout)
}
func lissajous(out io.Writer){
const(
cycles = 5 //完整的x振荡器变化的个数
res = 0.001 //角度分辨率
size = 100 //图像画布包含[-size..size]
nframes = 64//动画中的帧数
delay = 8 //以10ms为单位的帧间延迟
)
freq := rand.Float64()*3.0 //y振荡器的相对频率
anim := gif.GIF{LoopCount:nframes}
phase := 0.0 // phase difference
for i := 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size*0.5),size+int(y*size+0.5),blackIndex)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) //注意:忽略编码错误
}
- 在导入那些由多段路径如
image/color
组成的包之后,使用路径最后的一段来引用这个包、所以变量color.White
属于image/color
包,gif.GIF
属于image/gif
包 - 表达式
[]color.Color{}
和gif.GIF{}
是复合字面量,即用一系列元素的值初始化Go的复合类型的紧凑表达方式。这里,第一个是slice,第二个是结构体。
1.5 获取一个URL
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)
}
}
http.Get
函数产生一个HTTP
请求,如果没有出错,返回结果存在响应结构resp
里面。其中的Body
域包含服务器端响应的一个可读取数据流。随后ioutil.ReadAll
读取整个相应结果并存入b
。关闭Body
数据流来避免资源泄露
,使用Printf
将响应输出到标准输出
1.6 获取多个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) //发送到通道ch
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)
}
- 使用
go
并发执行多个fetch
操作,这个进程使用的时间不超多耗时最长时间的获取任务,而不是所有获取任务总的时间。 - 通道是一种允许某一例程向另一例程传递指定类型的值的通信机制。main函数在一个goroutine中执行,然后go语句创建额外的goroutine。
- 当一个goroutine试图在一个通道上进行发送或接收操作时,它会阻塞,直到另一个goroutine试图进行接收或发送操作才传递值,并开始处理两个goroutine。
1.7 一个简单的Web服务器
server1.0
package main
import (
"fmt"
"log"
"net/http"
)
func main(){
http.HandleFunc("/",handler) //回升请求调用处理程序
log.Fatal(http.ListenAndServe("localhost:8000",nil))
}
//处理程序回显请求URL r的路径部分
func handler(w http.ResponseWriter, r *http.Request){
fmt.Fprintf(w,"URL.Path = %q\n",r.URL.Path)
}
- main函数将一个处理函数和以
/
开头的URL链接在一起,代表所有的URL使用这个函数,然后启动服务器监听进入8000端口处的请求。 - 一个请求由一个http.Request类型的结构体表示,他包含很多关联的域,其中一个是请求的URL。当一个请求到达时,它被转交给处理函数,并从请求的URL中提取路径部分(/hello),使用fmt.Printf格式化,然后作为响应发送回去
server2.0
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))
}
//处理程序回显请求URL r的路径部分
func handler(w http.ResponseWriter, r *http.Request){
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w,"URL.Path = %q\n",r.URL.Path)
}
//counter回显目前为止调用的次数
func counter(w http.ResponseWriter, r *http.Request){
mu.Lock()
fmt.Fprintf(w,"Count %d\n", count)
mu.Unlock()
}
- 请求/count调用counter,其他的调用handler。以
/
结尾的处理模式匹配所有含有这个前缀的URL。 - 两个并发的请求试图同时更新计数值count,他可能会不一致的增加,程序会产生一个严重的竞态bug。为避免该问题,必须确保最多只有一个goroutine在同一时间访问变量,这正是mu.Lock()和mu.Unlock()语句的作用
server3.0
处理函数可以报告他接收到的消息头和表单数据,这样可以方便服务器审查和调试请求:
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\n] = %q\n",k,v)
}
}
- 这里是在if语句中嵌套调用ParseForm的,Go允许一个简单的语句(如一个局部变量声明)跟在if条件的后面,这在错误处理的时候特别有用。
也可以这样写:
err := r.ParseForm() if err != nil { log.Print(err) }
但是合并后的语句更短而且可以缩小err变量的作用域
1.8 其他内容
控制流
if、for已经在前面介绍过,在这里提一嘴switch语句,它是多路分支控制。
switch coinflip(){
case "heads":
heads++
case "tails":
tails++
default:
fmt.Println("landed on edge!")
}
- 默认的case语句(default)可以放在任何位置,case语句不想C语言那样从上到下贯穿执行(fallthrough语句可以改写这个行为)。
func Signum(x int) int {
switch{
case x>0:
return +1
default:
return 0
case x<0:
return -1
}
}
- switch语句不需要操作数,他就像一个case语句列表,每条case语句都是一个布尔表达式:
- 上面的switch语句称为无标签(tagless)选择,他等价于switch true。
- 与for和if语句类似,switch可以包含一个可选的简单语句:一个短变量声明,一个递增或赋值语句,或者一个函数调用,用来判断条件前设置一个值。
命名类型
type声明给已有类型命名。因为结构体类型通常很长,所以他们基本上都独立命名。
type Point struct{
X, Y int
}
var p Point
指针
Go提供了指针,它的值是变量d额地址。在一些语言(比如C)中,指针基本是没有约束的。其他语言中,指针称为"引用",并且除了到处传递之外,它不能做其他的事情。Go做了一个折中,指针显式可见。使用&操作符可以获取一个变量的地址,使用*操作符可以获取指针引用的变量的值,但是指针不支持算术运算。
方法和接口
一个关联了命名类型的函数称为方法。Go里面的方法可以关联到几乎所有的命名类型。接口可以用相同的方式处理不同的具体类型的抽象类型,它基于这些类型所包含的方法,而不是类型的描述或实现。