go圣经笔记——入门

文章目录


配置下idea环境(go版本1.17.1),按照go语言圣经目录进行学习。

go圣经笔记——入门

Hello, World

package main

import "fmt"

func main() {
	/* 这是我的第一个简单的程序 */
	fmt.Println("Hello, World!")
}

Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(译注:静态编译)。Go语言提供的工具都通过一个单独的命令go调用,go命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。

$ go run helloworld.go 

编译指令

$ go build helloworld.go 

Mac编译生成helloworld.exec可执行文件。

package

Go语言的代码通过(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成,目录定义包的作用。每个源文件都以一条package声明语句开始,这个例子里就是package main,表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。

Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。

必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包(译注:Go语言编译过程没有警告信息,争议特性之一)。

Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响Go代码的正确解析(译注:比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字breakcontinuefallthroughreturn中的一个、运算符和分隔符++--)]}中的一个)。举个例子,函数的左括号{必须和func函数声明在同一行上,且位于末尾,不能独占一行,而在表达式x + y中,可在+后换行,不能在+前换行(译注:以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编译错误)。换行符的必须有(行末有可解析的关键字或字符)

命令行参数

os包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从os包的Args变量获取;os包外部使用os.Args访问该变量。

os.Args变量是一个字符串(string)的切片(slice)(译注:slice和Python语言中的切片类似,是一个简版的动态数组),切片是Go语言的基础概念,稍后详细介绍。现在先把切片s当作数组元素序列,序列的长度动态变化,用s[i]访问单个元素,用s[m:n]获取子序列(译注:和python里的语法差不多)。

下面是Unix里echo命令的一份实现,echo把它的命令行参数打印成一行。程序导入了两个包,用括号把它们括起来写成列表形式,而没有分开写成独立的import声明。两种形式都合法,列表形式习惯上用得多。

package main

import (
   "fmt"
   "os"
)

func main() {
   var s, sep string
   for i := 1; i < len(os.Args); i++ { //符号:=是短变量声明(short variable declaration)的一部分
      s += sep + os.Args[i]
      sep = " "
   }
   fmt.Println(s)
}
for initialization; condition; post {
    // zero or more statements
}

for循环三个部分不需括号包围。大括号强制要求,左大括号必须和post语句在同一行。

initialization语句是可选的,在循环开始前执行。initalization如果存在,必须是一条简单语句(simple statement),即,短变量声明、自增语句、赋值语句或函数调用。condition是一个布尔表达式(boolean expression),其值在每次循环迭代开始时计算。如果为true则执行循环体语句。post语句在循环体执行结束后执行,之后再次对condition求值。condition值为false时,循环结束。

for循环的这三个部分每个都可以省略,如果省略initializationpost,分号也可以省略,condition也可以省略(无限循环)。

// a traditional "while" loop
for condition {
    // ...
}

for循环的另一种形式,在某种数据类型的区间(range)上遍历,如字符串或切片。

os.Args的第一个元素:os.Args[0],是命令本身的名字;其它的元素则是程序启动时传给它的参数。s[m:n]形式的切片表达式,产生从第m个元素到第n-1个元素的切片,下个例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表达式的m或n,会默认传入0或len(s),因此前面的切片可以简写成os.Args[1:]

echo的第二版本展示了这种形式:

package main

import (
   "fmt"
   "os"
)

func main() {
   s, sep := "", ""
   for _, arg := range os.Args[1:] {
      s += sep + arg
      sep = " "
   }
   fmt.Println(s)
}

Go语言中这种情况的解决方法是用空标识符(blank identifier),即_(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。大多数的Go程序员都会像上面这样使用range_echo程序,因为隐式地而非显式地索引os.Args,容易写对。只是简洁了一点。

echo的这个版本使用**一条短变量声明来声明并初始化s**和seps,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价:

s := ""
var s string
var s = ""
var s string = ""

用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。

每次循环迭代字符串s的内容都会更新。+=连接原字符串、空格和下个参数,产生新字符串,并把它赋值给ss原来的内容已经不再使用,将在适当时机对它进行垃圾回收。

如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用strings包的Join函数:

func main() {
    fmt.Println(strings.Join(os.Args[1:], " "))
}

稍微看了下Join函数,不会产生新的字符串。

func Join(elems []string, sep string) string {
   switch len(elems) {
   case 0:
      return ""
   case 1:
      return elems[0]
   }
   n := len(sep) * (len(elems) - 1)
   for i := 0; i < len(elems); i++ {
      n += len(elems[i])
   }

   var b Builder
   b.Grow(n)
   b.WriteString(elems[0])
   for _, s := range elems[1:] {
      b.WriteString(sep)
      b.WriteString(s)
   }
   return b.String()
}

查找重复行

对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为dup的程序的三个版本;灵感来自于Unix的uniq命令,其寻找相邻的重复行。

dup的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入if语句,map数据类型以及bufio包。

// Dup1 prints the text of each line that appears more than
// once in the standard input, preceded by its count.
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    counts := make(map[string]int)
    input := bufio.NewScanner(os.Stdin)
    for input.Scan() {
        counts[input.Text()]++
    }
    // NOTE: ignoring potential errors from input.Err()
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

dup程序的下个版本读取标准输入或是使用os.Open打开各个具名文件,并操作它们。

// Dup2 prints the count and text of lines that appear more than once
// in the input.  It reads from stdin or from a list of named files.
// 打开多个文件
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)
            // 如果err等于内置值nil(译注:相当于其它语言里的NULL),那么文件被成功打开。否则打印
            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)
        }
    }
}

func countLines(f *os.File, counts map[string]int) {
    input := bufio.NewScanner(f)
    for input.Scan() {
        counts[input.Text()]++
    }
    // NOTE: ignoring potential errors from input.Err()
}

注意countLines函数在其声明前被调用。函数和包级别的变量(package-level entities)可以任意顺序声明,并不影响其被调用。(译注:最好还是遵循一定的规范)

map是一个由make函数创建的数据结构的引用。map作为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的map引用看到。在我们的例子中,countLines函数向counts插入的值,也会被main函数看到。(译注:类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)

批注:在其声明前被调用,我不能理解。编译型语言,顺序无关紧要。但该说法有误吧。

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)
        }
    }
}

使用了库函数bufioio/ioutil。

GIF动画

下面的程序会演示Go语言标准库里的image这个package的用法,我们会用这个包来生成一系列的bit-mapped图,然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形(Lissajous figures),这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线,比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。

// Lissajous generates GIF animations of random Lissajous figures.
package main

import (
  "image"
  "image/color"
  "image/gif"
  "io"
  "math"
  "math/rand"
  "os"
  "time"
)

var palette = []color.Color{color.White, color.Black}

const (
  whiteIndex = 0 // first color in palette
  blackIndex = 1 // next color in palette
)

func main() {
  // The sequence of images is deterministic unless we seed
  // the pseudo-random number generator using the current time.
  // Thanks to Randall McPherson for pointing out the omission.
  rand.Seed(time.Now().UTC().UnixNano())
  lissajous(os.Stdout)
}

func lissajous(out io.Writer) {
  const (
    cycles  = 5     // number of complete x oscillator revolutions
    res     = 0.001 // angular resolution
    size    = 100   // image canvas covers [-size..+size]
    nframes = 64    // number of animation frames
    delay   = 8     // delay between frames in 10ms units
  )

  freq := rand.Float64() * 3.0 // relative frequency of y oscillator
  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) // NOTE: ignoring encoding errors
}
$ go build gopl.io/ch1/lissajous
$ ./lissajous >out.gif

go圣经笔记——入门

获取URL

Go语言在net这个强大package的帮助下可以更简单地用网络收发信息,还可以建立更底层的网络连接,编写服务器程序。在这些情景下,Go语言原生的并发特性(在第八章中会介绍)显得尤其好用。

为了最简单地展示基于HTTP获取信息的方式,下面给出一个示例程序fetch,这个程序将获取对应的url,并将其源文本打印出来;这个例子的灵感来源于curl工具(译注:unix下的一个用来发http请求的工具,具体可以man curl)。当然,curl提供的功能更为复杂丰富,这里只编写最简单的样例。

// Fetch prints the content found at a 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)
    }
}

1.6. 并发获取多个URL

Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题,在第八章和第九章中会专门讲到。这里我们只浅尝辄止地来体验一下Go语言里的goroutine和channel。

下面的例子fetchall,和前面小节的fetch程序所要做的工作基本一致,fetchall的特别之处在于它会同时去获取所有的URL,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的fetch程序执行时间则是所有任务执行时间之和。

// Fetchall fetches URLs in parallel and reports their times and sizes.
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) // start a goroutine
    }
    for range os.Args[1:] {
        fmt.Println(<-ch) // receive from channel 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) // send to channel ch
        return
    }
    nbytes, err := io.Copy(ioutil.Discard, resp.Body)
    resp.Body.Close() // don't leak resources
    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)
}

之后重点研究。类比java的线程模型。

Web服务

Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中,我们会展示一个微型服务器,这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello ,那么响应是URL.Path = “hello”。

// Server1 is a minimal "echo" server.
package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", handler) // each request calls handler
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the request URL r.
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

在这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。比如,下面这个版本输出了同样的内容,但是会对请求的次数进行计算;对URL的请求结果会包含各种URL被访问的总次数,直接对/count这个URL的访问要除外。

// Server2 is a minimal "echo" and counter server.
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))
}

// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    count++
    mu.Unlock()
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    fmt.Fprintf(w, "Count %d\n", count)
    mu.Unlock()
}

handler函数会把请求的http头和请求的form数据都打印出来,这样可以使检查和调试这个服务更为方便:

// handler echoes the HTTP request.
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)
    }
}

本章要点

命名类型

类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长,所以我们总要给这种struct取一个名字。本章中就有这样一个例子,二维点类型:

type Point struct {
    X, Y int
}
var p Point

指针

Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中,比如C语言,指针操作是完全不受约束的。在另外一些语言中,指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。

方法和接口

方法是和命名类型关联的一类函数。Go语言里比较特殊的是方法可以被关联到任意一种命名类型

packages

Go语言提供了一些很好用的package,并且这些package是可以扩展的。Go语言社区已经创造并且分享了很多很多。所以Go语言编程大多数情况下就是用已有的package来写我们自己的代码。

上一篇:SV学习笔记—包package的使用


下一篇:原创|批处理|Monkey自动测试工具批处理版