《The Go Programming Language》 知识点记载,学习笔记、章节练习与个人思考。前言 · Go语言圣经 (itsfun.top)

标题后标记了小丑符号的表示还没写。

Hello, World

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


命令行参数

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

练习 1.3

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

如果连接涉及的数据量很大,这种方式代价高昂。所以我们使用strings包的Join函数

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

练习 1.3: 做实验测量潜在低效的版本和使用了strings.Join的版本的运行时间差异。(1.6节讲解了部分time包,11.4节展示了如何写标准测试程序,以得到系统性的性能评测。)这里使用 time 包计算运行时间。

// ^ 练习 1.3: 做实验测量潜在低效的版本和使用了strings.Join的版本的运行时间差异。
package main

import (
	"fmt"
	"strconv"
	"strings"
	"time"
)

func main() {
	args := make([]string, 10000)
	for i := 0; i < 10000; i++ {
		args = append(args, strconv.Itoa(i+1))
	}
	var s string
	start := time.Now()
	for i := range args {
		s += args[i]
		s += " "
	}
	// fmt.Println(s) // ^ 因为切片数量过大先不打印就只做合并操作
	t := time.Since(start)
	fmt.Println("elapsed time", t)
	/* -------------------------------------------------------------------------- */
	start = time.Now()
	s = strings.Join(args, " ")
	fmt.Println(s)
	t = time.Since(start)
	fmt.Println("elapsed time", t)
}
elapsed time 230.8618ms
elapsed time 0s

可以看到前者耗费了0.23秒,后者几乎没有耗费时间

为了深入 (strings.Join) 方法,我们需要先深入 (strings.Builder) 类型


strings.Builder 源码解析

存在意义

使用 (strings.Builder),避免频繁创建字符串对象,进而提高性能

(Source file) https://go.dev/src/strings/builder.go


在上面的高耗时案例中,与许多支持 (string) 类型的语言一样,(golang) 中的 (string) 类型也是只读且不可变的( (string) 类型笔记 Go xmas2020 学习笔记 04、Strings - 小能日记 - 博客园 )。因此,这种拼接字符串的方式会导致大量的string创建、销毁和内存分配。如果你拼接的字符串比较多的话,这显然不是一个正确的姿势。


(strings.Builder) 出来以前,我们是用 (bytes.Buffer) 来进行优化的。

func main() {
    ss := []string{
        "A",
        "B",
        "C",
    }

    var b bytes.Buffer
    for _, s := range ss {
        fmt.Fprint(&b, s)
    }

    print(b.String())
}

这里使用 var b bytes.Buffer 存放最终拼接好的字符串,一定程度上避免上面 (str) 每进行一次拼接操作就重新申请新的内存空间存放中间字符串的问题。

但这里依然有一个小问题: (b.String()) 会有一次 ([ ]byte -> string) 类型转换。而这个操作是会进行一次内存分配和内容拷贝的。


原理解析

Golang 官方将 (strings.Builder) 作为一个(feature) 引入。

  1. (byte.Buffer) 思路类似,既然 (string) 在构建过程中会不断的被销毁重建,那么就尽量避免这个问题,底层使用一个 (buf [ ]byte) 来存放字符串的内容。
  2. 对于写操作,就是简单的将 (byte) 写入到 (buf) 即可。
  3. 为了解决 (bytes.Buffer.String()) 存在的 ([ ]byte -> string) 类型转换和内存拷贝问题,这里使用了一个(unsafe.Pointer) 的内存指针转换操作,实现了直接将 (buf [ ]byte)转换为 (string) 类型,同时避免了内存充分配的问题。
  4. 如果我们自己来实现 (strings.Builder) , 大部分情况下我们完成前3步就觉得大功告成了。但是标准库做得要更近一步。我们知道 Golang 的堆栈在大部分情况下是不需要开发者关注的,如果能够在栈上完成的工作逃逸到了堆上,性能就大打折扣了。因此,(copyCheck) 加入了一行比较 (hack) 的代码来避免 (buf) 逃逸到堆上。Go 栈、堆的知识可看 GopherCon SG 2019 "Understanding Allocations" 学习笔记 - 小能日记 - 博客园

常用方法

  • String方法返回Builder构建的数据
  • Len方法返回字节数组占据的字节数,1个汉字三个字节
  • Cap方法返回字节数组分配的内存空间大小
  • Reset方法将Builder重置为初始状态
  • Write方法将字节数组加添加到buf数组后面
  • WriteByte将字节c添加到buf数组后边
  • WriteRune将rune字符添加到buf数组后面
  • WriteString将字符串添加到buf数组后面

写入方法

(bytes.Buffer) 也支持这四个写入方法。

func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)

image-20220408152322464

strings.Builder organizes the content based on the internal slice to organize. When you call write-methods, they append new bytes to inner-slice. If the slice’s capacity is reached, Go will allocate a new slice with different memory space and copy old slice to a new one. It will take resource to do when the slice is large or it may create the memory issue

The rune and a character of string can be more than 1 bytes when you WriteRune() or WriteString()

我们可以预定义切片的容量来避免重新分配。

扩容方法

追加内容也有讲究,因为底层是 (slice),追加数据时有可能引起 (slice) 扩容。一般的优化方案是为 (slice) 初始化合理的空间,避免多次扩容复制。(Builder) 也提供了预分配内存的方法,如 (Grow) 方法。

func (b *Builder) grow(n int) {
	buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
	copy(buf, b.buf)
	b.buf = buf
}

func (b *Builder) Grow(n int) {
	b.copyCheck()
	if n < 0 {
		panic("strings.Builder.Grow: negative count")
	}
	if cap(b.buf)-len(b.buf) < n {
		b.grow(n)
	}
}

注意扩容的容量和 (slice) 直接扩容两倍的方式略有不同,它是2*cap(b.buf)+n,之前容量的两倍加 (n)

  • 如果容量是10,长度是5,调用 (Grow(3))结果是什么?当前容量足够使用,没有任何操作;
  • 如果容量是10,长度是5,调用 (Grow(7))结果是什么?剩余空间是5,不满足7个扩容空间,底层需要扩容。扩容的时候按照之前容量的两倍再加 (n) 的新容量扩容,结果是 (2*10+7=27)

String() 方法

func (b *Builder) String() string {
	return *(*string)(unsafe.Pointer(&b.buf))
}

先获取 ([ ]byte) 地址,然后转成字符串指针,然后再取地址。

从 ptype 输出的结构来看,string 可看做 [2]uintptr,而 [ ]byte 则是 [3]uintptr,这便于我们编写代码,无需额外定义结构类型。如此,str2bytes 只需构建 [3]uintptr{ptr, len, len},而 bytes2str 更简单,直接转换指针类型,忽略掉 cap 即可。


禁止复制

type Builder struct {
	addr *Builder // of receiver, to detect copies by value
	buf  []byte
}

(Builder) 的底层数据,它还有个字段 (addr) ,是一个指向 (Builder) 的指针。默认情况是它会指向自己。

b.addr = (*Builder)(noescape(unsafe.Pointer(b)))

(copyCheck) 用来保证复制后不允许修改的逻辑。仔细看下源码,如果 (addr) 是空,也就是没有数据的时候是可以被复制后修改的,一旦那边有数据了,就不能这么搞了。在 (Grow)(Write)(WriteByte)(WriteString)(WriteRune) 这五个函数里都有这个检查逻辑。

var b1 strings.Builder
b2 := b1
b2.WriteString("DEF")
b1.WriteString("ABC")
// b1 = ABC, b2 = DEF

var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
b2.WriteString("DEF") 

代码将会报错 illegal use of non-zero Builder copied by value

下面的意思是拷贝过来的Builder进行添加修改,会造成其他Builder的修改。

When we copy the Builder, we clone the pointer of the slice but they still point to the old array. The problem will be occurs when you try to Write something to copied Builder or source Builder, the other’s content will be affects. That’s reason why strings.Builder prevent copy actions.

image-20220408152623591


我们可以使用 (Reset) 方法对 (addr、buf) 置空。下面拷贝了使用 (Reset) 后不会报错。

var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
fmt.Println(b2.Len())    // 3
fmt.Println(b2.String()) // ABC
b2.Reset()
b2.WriteString("DEF")
fmt.Println(b2.String()) // DEF

线程不安全

func main() {
	var b strings.Builder
	var n int32
	var wait sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wait.Add(1)
		go func() {
			atomic.AddInt32(&n, 1)
			b.WriteString("1")
			wait.Done()
		}()
	}
	wait.Wait()
	fmt.Println(len(b.String()), n)
}
905 1000

结果是 (905 1000),并不都是 (1000) 。如果想保证线程安全,需要在 (WriteString) 的时候加锁。


io.Writer 接口

(strings.Builder) 实现了 (io.Writer) 接口。可以使用在很多例子中

  • io.Copy(dst Writer, src Reader) (written int64, err error)
  • bufio.NewWriter(w io.Writer) *Writer
  • fmt.Fprint(w io.Writer, a …interface{}) (n int, err error)
  • func (r *http.Request) Write(w io.Writer) error
  • and other libraries that uses (io.Writer)

代码

func main() {
	var b strings.Builder
	fmt.Printf("%v", b)
	fmt.Println(b.Len(), b.Cap())
	for i := 3; i >= 1; i-- {
		fmt.Fprintf(&b, "%d#", i)
		fmt.Printf("%vn", b)
		fmt.Println(b.Len(), b.Cap())
	}
	b.WriteString("Hello")
	fmt.Printf("%vn", b)
	fmt.Println(b.Len(), b.Cap())
	fmt.Println(b.String())

	// b.Grow(5) // ^ 扩容
	b.Grow(88) // ^ 扩容
	fmt.Printf("%vn", b)
	fmt.Println(b.Len(), b.Cap())

	fmt.Println(unsafeEqual("Hello", []byte{72, 101, 108, 108, 111}))
}

func unsafeEqual(a string, b []byte) bool {
	bbp := *(*string)(unsafe.Pointer(&b))
	return a == bbp
}
{<nil> []}0 0
{0xc0001223a0 [51 35]}
2 8
{0xc0001223a0 [51 35 50 35]}
4 8
{0xc0001223a0 [51 35 50 35 49 35]}
6 8
{0xc0001223a0 [51 35 50 35 49 35 72 101 108 108 111]}
11 16
3#2#1#Hello
{0xc0001223a0 [51 35 50 35 49 35 72 101 108 108 111]}
11 120
true

strings.Join 源码解析

实现原理

// Join concatenates the elements of its first argument to create a single string. The separator
// string sep is placed between elements in the resulting string.
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()
}

前面计算出整个字符串需要的长度 (n),然后创建 (strings.Builder) 并通过 (Grow) 方法直接扩容,大小为 (0*2+n)(n) ,然后通过 (WriteString) 方法写入,最后调用 (String) 方法返回字符串。只构造了一次字符串对象


查找重复的行

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: %vn", err)
                continue
            }
            countLines(f, counts)
            f.Close()
        }
    }
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%dt%sn", 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()
}

bufio.Scanner

程序使用短变量声明创建bufio.Scanner类型的变量input

input := bufio.NewScanner(os.Stdin)

该变量从程序的标准输入中读取内容。每次调用input.Scan(),即读入下一行,并移除行末的换行符;读取的内容可以调用input.Text()得到。Scan函数在读到一行时返回true,不再有输入时返回false

map 传递

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

为了打印结果,我们使用了基于range的循环,并在counts这个map上迭代。跟之前类似,每次迭代得到两个结果,键和其在map中对应的值。map的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。

ioutil.ReadFile

ReadFile函数一口气把全部输入数据读到内存中,返回一个字节切片(byte slice),必须把它转换为string,才能用strings.Split分割。

练习 1.4

练习 1.4: 修改dup2,出现重复的行时打印文件名称。

func main() {
	counts := make(map[string]int)
	counts_fileid := make(map[string]string)
	files := os.Args[1:]
	if len(files) == 0 {
		countLines(os.Stdin, counts, counts_fileid)
	} else {
		for _, arg := range files {
			f, err := os.Open(arg)
			if err != nil {
				fmt.Fprintf(os.Stderr, "dup2: %vn", err)
				continue
			}
			countLines(f, counts, counts_fileid)
			f.Close()
		}
	}
	for line, n := range counts {
		if n > 1 {
			fmt.Printf("%dt%st%vn", n, line, counts_fileid[line])
		}
	}
}

func countLines(f *os.File, counts map[string]int, counts_fileid map[string]string) {
	input := bufio.NewScanner(f)
	for input.Scan() {
		counts[input.Text()]++
		counts_fileid[input.Text()] = f.Name()
	}
}
hello Server
world
hello Server
123456
cat
123456
hello Server
hello
world
123456
3       hello Server    data.txt
2       world   data2.txt
3       123456  data2.txt

为什么 Map 中 Key 是无序的
内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/linxiaoxu/p/16187331.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程