Go 语言之旅(一)

142 阅读7分钟

最近要学习分布式系统,需要用go语言做lab。记录一下自己学习go语言的过程,以及学习过程中遇到的一些问题。

Go 语言之旅是Go语言官方提供的学习go语言基本语法的指南,是新手学习Go语言非常好的材料。花半天到一天的时间过一遍,可以快速上手go语言。

私以为学习一门编程语言的最好方式就是直接“看代码”,通过理解代码实现的特定功能去理解这门语言。掌握基本语法之后,就可以直接上手写了,配合文档查漏补缺。这样学习编程语言比之通过看视频学习来的更快,更容易得到反馈。

下面是Go 语言之旅中练习的答案。仅供参考。

练习:循环与函数

循环10次

package main
import (
  "fmt"
  "math"
)
​
func Sqrt(x float64) float64 {
     // 赋初值 1
     z := float64(1)
     // 迭代 10 次,更新 z 值
     for i := 0; i <= 10; i++ {
          // 根据牛顿法公式计算 z 的新值
          z = z - (z*z - x) / (2 * z)
     }
     return z
}
​
func main() {
     fmt.Println(Sqrt(2))
     fmt.Println(math.Sqrt(2))
}

无限接近

package main
​
import (
  "fmt"
  "math"
)
​
func Sqrt(x float64) float64 {
  z := 1.0    
  count := 0    //循环次数          
  temp := z - (z*z - x) / (2*z)
  // 反复更新z值,直到满足精度要求
  for math.Abs(temp - z) > 1e-14 {
    z = temp
    temp = z - (z*z - x) / (2*z)
    count += 1
  }
  fmt.Println("the count of loo :", count)
  return z
}
​
func main() {
  fmt.Println(Sqrt(2))
  fmt.Println(math.Sqrt(2))
}
​

练习:切片

package main
​
import (
  "fmt"
  "golang.org/x/tour/pic"
)
​
// Pic 函数接受两个整数参数 dx 和 dy 表示图像大小,
// 返回一个二维数组,该数组包含每个像素的灰度值。
func Pic(dx, dy int) [][]uint8 {
  // 使用 make 创建一个长度为 dy 的切片,
  // 每个元素都是一个长度为 dx 的 uint8 类型切片。
  a := make([][]uint8, dy)
  
  fmt.Println("slice:", a)
  
  // 对切片 a 进行循环迭代,
  // x 代表 a 中的每个元素(即每个一维 uint8 类型切片)。
  for x := range a {      
    // 使用 make 创建一个长度为 dx 的 uint8 类型切片 b。
    b := make([]uint8, dx)  
    
    // 对切片 b 进行循环迭代,
    // y 代表 b 中的每个元素(即每个像素的横坐标)。
    for y := range b {    
      // 将当前像素的灰度值设为其横纵坐标之积。
      b[y] = uint8(x * y) 
    }
    
    // 将切片 b 添加到切片 a 中。
    a[x] = b        
  }
  
  // 返回形如 [[0 0 0 ... 0] [0 1 2 ... 255] ... [0 2 4 ... 510]] 的二维 uint8 类型切片。
  return a
}
​
func main() {
  // 使用函数 Show 显示 Pic 函数返回的图像。
  pic.Show(Pic)       
}
​

练习:映射

package main
​
import (
    "golang.org/x/tour/wc"
    "strings" 
)
​
// 定义函数 WordCount,接收一个字符串类型的参数 s,返回一个 map 类型的结果
func WordCount(s string) map[string]int {
    m := make(map[string]int) // 创建一个空 map,这跟上面a := make([][]uint8, dy)的含义是不一样的,
    for _, c := range strings.Split(s, " ") { // 将字符串按空格分割成单词,并遍历这些单词
        m[c] += 1 // 统计每个单词出现的次数,如果该单词在 map 中不存在,则初始化为 0,再加上 1
    }
    return m // 返回统计好的 map
}
​
// 主函数
func main() {
    wc.Test(WordCount) // 测试 WordCount 函数是否正确
}
​

练习:斐波纳契闭包

package main
​
import "fmt"// 返回一个“返回类型为int的函数” ,将闭包函数作为值返回
func fibonacci() func() int {
    a, b := 0, 1 //定义初始的两个值
    return func() int {
        c := a
        a, b = b, a+b
        return c
    }
}
​
func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

练习:Stringer

前提知识:

在 Go 中,如果某个类型实现了 String() 方法,那么在使用 %v 格式化字符串时,将会自动调用该类型的 String() 方法。所以在这段代码中,当我们使用 %v 格式化 ip 变量时,Go 语言会自动调用 IPAddr 类型的 String() 方法,然后将其返回值插入到格式化字符串中。

package main
​
import "fmt"type IPAddr [4]bytefunc (ip IPAddr) String() string{
    return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])//按格式拼接字符串
}
​
​
func main() {
  hosts := map[string]IPAddr{
    "loopback":  {127, 0, 0, 1},
    "googleDNS": {8, 8, 8, 8},
  }
  for name, ip := range hosts {
    fmt.Printf("%v: %v\n", name, ip)
  }
}
​

练习:错误

前提知识:

在调用fmt包中的打印函数(包括:fmt.Println fmt.Print fmt.Printf fmt.Sprint fmt.Sprintf fmt.Sprintln fmt.Fprint fmt.Fprintf fmt.Fprintln)时,如果传入的参数实现了Error()方法,那么就会自动调用该方法并将返回结果输出到控制台。后面给出源码示例。

package main
​
import (
  "fmt"
  "math"
)
​
type ErrNegativeSqrt float64func (e ErrNegativeSqrt) Error() string {
  return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}
​
​
func Sqrt(x float64) (float64, error) {
  if x < 0 {
    return 0, ErrNegativeSqrt(x)
  }
  
  z := 1.0    
  count := 0    //循环次数          
  temp := z - (z*z - x) / (2*z)
  for math.Abs(temp - z) > 1e-14 {
    z = temp
    temp = z - (z*z - x) / (2*z)
    count += 1
  }
  fmt.Println("the count of loo :", count)
  return z, nil
}
​
func main() {
  fmt.Println(Sqrt(2))
  fmt.Println(Sqrt(-2))    
}

以Println为例,查看函数如何最终调用Error方法。源码地址:# Source file src/fmt/print.go

Println中调用了Fprintln

image-20230616222405176

Fprintln中调用了doPrint

image-20230616222332753

doPrint中调用了printArg

image-20230616222250007

printArg方法中调用了handleMethods方法

image-20230616221816377

如果参数是error类型则调用它的Error方法。

image-20230616222112732

为什么要将e转换为float64(e)。

根本原因:fmt.Sprintf会在内部调用ErrNegativeSqrt类Error()方法,在Error方法中又会继续调用下去,这样就产生了死循环。

至于为什么fmt.Sprintf会去调用Error()方法,除了通过上面源码的方式证明外,也可以直观很理解,因为 %v 格式化标识符只能用于常规(非自定义)类型的值,而此时的e是一个ErrNegativeSqrt自定义类,所以编译器会试图将ErrNegativeSqrt转换为string类型,ErrNegativeSqrt实现了error接口,所以就会去调用Error()方法。;

练习:Reader

package main
​
import "golang.org/x/tour/reader"type MyReader struct{}
​
func (reader MyReader) Read(b []byte) (n int, err error){
  for i := range b {
        b[i] = 'A'
    }
    return len(b), nil
}
​
func main() {
  reader.Validate(MyReader{})
}
​

练习:rot13Reader

package main
​
import (
  "io"
  "os"
  "strings"
)
​
type rot13Reader struct {
  r io.Reader
}
​
func (r rot13Reader) Read(b []byte) (n int, err error){
  //读取数据
  n, err = r.r.Read(b)
  //应用 rot13 代换密码对数据流进行修改。
  for i := 0; i < n; i++ {
    c := b[i]
    if c >= 'a' && c <= 'z' {
      b[i] = 'a' + (c-'a'+13)%26
    } else if c >= 'A' && c <= 'Z' {
      b[i] = 'A' + (c-'A'+13)%26
    }
  }
  return n, err
}
​
func main() {
  s := strings.NewReader("Lbh penpxrq gur pbqr!")
  r := rot13Reader{s}
  io.Copy(os.Stdout, &r)
}
​

练习:图像

package main
​
import (
    "image"
    "image/color"
    "golang.org/x/tour/pic"
)
​
type myImage struct {
    width  int
    height int
}
​
func (m myImage) Bounds() image.Rectangle {
    return image.Rect(0, 0, m.width, m.height)    //两点定义一个矩形Pt(x0, y0), Pt(x1, y1)
}
​
func (m myImage) ColorModel() color.Model {
    return color.RGBAModel    //RGBA即(红、绿、蓝、Alpha)Alpha 通道表示透明度
}
​
func (m myImage) At(x, y int) color.Color {
    v := uint8((x * y)) 
    return color.RGBA{v, v, 255, 255}
}
​
func main() {
    m := myImage{256, 256}
    pic.ShowImage(m)
}
​

练习:等价二叉查找树

package main
​
import (
  "golang.org/x/tour/tree"
  "fmt"
)
​
// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int) {
  if t != nil {
    Walk(t.Left, ch)
    ch <- t.Value
    Walk(t.Right, ch)
  }
}
​
// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool {
  ch1, ch2 := make(chan int), make(chan int)
  go Walk(t1, ch1)
  go Walk(t2, ch2)
  for i := 0; i < 10; i++ {
    if <-ch1 != <-ch2 {
      return false
    }
  }
  return true
}
​
func main() {
  fmt.Println(Same(tree.New(1), tree.New(1)))
  fmt.Println(Same(tree.New(1), tree.New(-11)))
}

练习:Web 爬虫

package main
​
import (
  "fmt"
  "sync"
)
​
// Cache 是一个线程安全的数据结构,用于跟踪已经访问过的url
type Cache struct {
  v map[string]bool   // map用于存储已经访问过的url
  mux sync.Mutex      // 互斥锁用于在并发环境中保护map数据结构
}
// cache 是全局的Cache实例,用于在整个程序中跟踪已访问的url
var cache = Cache{v: make(map[string]bool)}
// visited 方法检查给定的url是否已经被访问过
func (cache *Cache)visited(key string) bool {
  cache.mux.Lock()
  _, ok := cache.v[key]
  cache.mux.Unlock()
  return ok
}
// visit 方法标记一个url已被访问
func (cache *Cache)visit(key string) {
  cache.mux.Lock()
  cache.v[key] = true
  cache.mux.Unlock()
}
​
​
type Fetcher interface {
  Fetch(url string) (body string, urls []string, err error)
}
​
// Crawl 函数抓取给定的url和相关的链接,深度为depth。
// 如果给定的url已经被访问过,或者深度为0,则Crawl会立即返回。
func Crawl(url string, depth int, fetcher Fetcher) {
  if depth <= 0 || cache.visited(url) {
    return
  }
  body, urls, err := fetcher.Fetch(url)
  cache.visit(url)
  
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Printf("found: %s %q\n", url, body)
  done := make(chan bool)               // 创建一个channel用于跟踪goroutine的完成
  for _, u := range urls {
    go func(u string){
      Crawl(u, depth-1, fetcher)
      done <- true                      // 当爬取完成时,发送一个信号到channel
    }(u)
  }
  for range urls {
    <- done                             // 等待一个信号,表明一个goroutine已经完成
  }
  return
}
​
func main() {
  Crawl("https://golang.org/", 4, fetcher)
}
​
// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult
​
type fakeResult struct {
  body string
  urls []string
}
​
func (f fakeFetcher) Fetch(url string) (string, []string, error) {
  if res, ok := f[url]; ok {
    return res.body, res.urls, nil
  }
  return "", nil, fmt.Errorf("not found: %s", url)
}
​
// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
  "https://golang.org/": &fakeResult{
    "The Go Programming Language",
    []string{
      "https://golang.org/pkg/",
      "https://golang.org/cmd/",
    },
  },
  "https://golang.org/pkg/": &fakeResult{
    "Packages",
    []string{
      "https://golang.org/",
      "https://golang.org/cmd/",
      "https://golang.org/pkg/fmt/",
      "https://golang.org/pkg/os/",
    },
  },
  "https://golang.org/pkg/fmt/": &fakeResult{
    "Package fmt",
    []string{
      "https://golang.org/",
      "https://golang.org/pkg/",
    },
  },
  "https://golang.org/pkg/os/": &fakeResult{
    "Package os",
    []string{
      "https://golang.org/",
      "https://golang.org/pkg/",
    },
  },
}
​