《Go语言实战》读书笔记

举报
山河已无恙 发表于 2022/04/19 00:17:45 2022/04/19
【摘要】 写在前面接触了一些云原生的东西,有些书里Demo写的GO,所以刷这本书之所以选择这本书,一是因为之前看过java系列的书,感觉不错。二是页数少,只有240页嗯,C忘的啥也没了,所以算是从零开始好多理论都不懂,这里先记下来,以后慢慢会懂。主要是《Go语言实战》读书笔记,学习环境简单描述小伙伴们 生活加油 ^_^傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。...

写在前面

  • 接触了一些云原生的东西,有些书里Demo写的GO,所以刷这本书
  • 之所以选择这本书,一是因为之前看过java系列的书,感觉不错。
  • 二是页数少,只有240页
  • 嗯,C忘的啥也没了,所以算是从零开始
  • 好多理论都不懂,这里先记下来,以后慢慢会懂。
  • 主要是《Go语言实战》读书笔记,学习环境简单描述
  • 小伙伴们 生活加油 ^_^

傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。--------王小波


环境准备

windows 安装

搭环境浪费了我好多时间,这里的话简单描述下

需要配置两个变量

  • GOPATH :项目路径
  • Path:添加GO环境路径,到bin\,可以看到一个go.exe那一层

测试

PS D:\Go> go version
go version go1.17.7 windows/amd64
PS D:\Go>

VsCode编译 需要安装两个插件:

名称: Go
ID: golang.go
说明: Rich Go language support for Visual Studio Code
版本: 0.31.1
发布者: Go Team at Google
VS Marketplace 链接: https://marketplace.visualstudio.com/items?itemName=golang.Go

名称: Golang Tools
ID: neonxp.gotools
说明: Tools for productive work
版本: 0.0.7
发布者: Alexander NeonXP Kiryukhin
VS Marketplace 链接: https://marketplace.visualstudio.com/items?itemName=NeonXP.gotools

简单测试一下:

package main

import "fmt"

func main() {
	fmt.Println("Hello, 世界")
}
-------------------------------------------------------------------------
[Running] go run "d:\GolandProjects\gopl.io-master\ch1\dup1\hello.go"
Hello, 世界

[Done] exited with code=0 in 1.312 seconds

Linux 安装

┌──[root@liruilongs.github.io]-[/]
└─$ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.7.linux-amd64.tar.gz
┌──[root@liruilongs.github.io]-[/]
└─$ export PATH=$PATH:/usr/local/go/bin
┌──[root@liruilongs.github.io]-[/]
└─$ go version
go version go1.17.7 linux/amd64
┌──[root@liruilongs.github.io]-[/]
└─$

第 1 章 关于 Go 语言的介绍

1.1 用Go解决现代编程难题

C C++这类语言提供了很快的执行速度,而 Ruby  Python 这类语言则擅长快速开发Go 语言在这两者间架起了桥梁,不仅提供了高性能的语言,同时也让开发更快速

Go 语言的语法简洁到只有几个关键字,便于记忆。
Go 语言的编译器速度非常快,有时甚至会让人感觉不到在编译。
Go 语言内置并发机制,所以不用被迫使用特定的线程库,就能让软件扩展,使用更多的资源。
Go 语言的类型系统简单且高效,不需要为面向对象开发付出额外的心智,让开发者能专注于代码复用
Go 语言还自带垃圾回收器,不需要用户自己管理内存。

1.1.1 开发速度

编译Go程序时,编译器只会关注那些直接被引用的库,而不是像Java、CC++那样,要遍历依赖链中所有依赖的库。因此,很多Go程序可以在 1 秒内编译完,在现代硬件上,编译整个 Go,语言的源码树只需要 20 秒。

因为没有从编译代码到执行代码的中间过程,用动态语言编写应用程序可以快速看到输出。代价是,动态语言不提供静态语言提供的类型安全特性,不得不经常用大量的测试套件来避免在运行的时候出现类型错误这类 bug

嗯,不是特别理解,所以为的安全特性是指什么,java的中的泛型类似

1.1.2 并发

Go语言对并发的支持是这门语言最重要的特性之一goroutine很像线程,但是它占用的内存远少于线程,使用它需要的代码更少。通道(channel)是一种内置的数据结构,可以让用户在不同的goroutine之间同步发送具有类型的消息这让编程模型更倾向于在goroutine之间发送消息,而不是让多个goroutine争夺同一个数据的使用权

让我们看看这些特性的细节。

1.goroutine

goroutine是可以与其他goroutine并行执行的函数,同时也会与主程序(程序的入口)并行执行。在其他编程语言中,你需要用线程来完成同样的事情,而在Go 语言中会使用同一个线程来执行多个 goroutine

2.通道

通道是一种数据结构,可以让goroutine之间进行安全的数据通信通道可以帮用户避免其他语言里常见的共享内存访问的问题。

并发的最难的部分就是要确保其他并发运行的进程、线程或goroutine不会意外修改用户的数据。当不同的线程在没有同步保护的情况下修改同一个数据时,总会发生灾难。

在其他语言中,如果使用全局变量或者共享内存,必须使用复杂的锁规则来防止对同一个变量的不同步修改。为了解决这个问题,通道提供了一种新模式,从而保证并发修改时的数据安全

通道这一模式保证同一时刻只会有一个goroutine修改数据。通道用于在几个运行的goroutine之间发送数据

在这里插入图片描述

3个goroutine,还有2个不带缓存的通道。第一个goroutine通过通道数据传给已经在等待的第二个goroutine。在两个goroutine间传输数据是同步的,一旦传输完成,两个goroutine都会知道数据已经完成传输。当第二个goroutine利用这个数据完成其任务后,将这个数据传给第三个正在等待的goroutine。这次传输依旧是同步的,两个goroutine都会确认数据传输完成。这种在goroutine之间安全传输数据的方法不需要任何锁或者同步机制。

感觉有点类似Javavolatile

需要强调的是,通道并不提供跨goroutine的数据访问保护机制。如果通过通道传输数据的一份副本,那么每个goroutine都持有一份副本,各自对自己的副本做修改是安全的。当传输的是指向数据的指针时,如果读和写是由不同的goroutine完成的,每个goroutine依旧需要额外的同步动作

1.1.3 Go 语言的类型系统

Go 语言提供了灵活的、无继承的类型系统,无需降低运行性能就能最大程度上复用代码

Go 开发者使用组合(composition)设计模式,只需简单地将一个类型嵌入到另一个类型

在 Go 语言中,一个类型由其他更微小的类型组合而成,避免了传统的基于继承的模型。

Go 语言还具有独特的接口实现机制,允许用户对行为进行建模,而不是对类型进行建模,在 Go 语言中,不需要声明某个类型实现了某个接口,编译器会判断一个类型的实例是否符合正在使用的接口

1.类型简单

Go 语言不仅有类似 int 和 string 这样的内置类型,还支持用户定义的类型。在 Go 语言中,用户定义的类型通常包含一组带类型的字段,用于存储数据。Go 语言的用户定义的类型看起来和 C 语言的结构很像

2.Go 接口对一组行为建模

接口用于描述类型的行为。如果一个类型的实例实现了一个接口,意味着这个实例可以执行一组特定的行为。

1.1.4 内存管理

不当的内存管理会导致程序崩溃或者内存泄漏,甚至让整个操作系统崩溃。Go语言拥有现代化的垃圾回收机制,

1.2 你好,Go

在线编译:https://go.dev/play/

package main

import "fmt"

func main() {
	fmt.Println("Hello, 世界")
}
-------------------------------------------------------------------------
[Running] go run "d:\GolandProjects\gopl.io-master\ch1\dup1\hello.go"
Hello, 世界

[Done] exited with code=0 in 1.312 seconds

第2章 快速开始一个Go程序

分层思想,类似常用的MVC,MVP,MVVM之类。

2.1 程序架构

在这里插入图片描述

2.2 main 包

package main

import (
	"log"
	"os"

	_ "github.com/goinaction/code/chapter2/sample/matchers"
	"github.com/goinaction/code/chapter2/sample/search"
)

// init is called prior to main.
func init() {
	// Change the device for logging to stdout.
	log.SetOutput(os.Stdout)
}

// main is the entry point for the program.
func main() {
	// Perform the search for the specified term.
	search.Run("president")
}

每个可执行的 Go 程序都有两个明显的特征。

  • 一个特征是声明的名为 main 的函数。
  • 第二个特征是程序的第 1 行的包名 main
import (
	"log"
	"os"

	_ "github.com/goinaction/code/chapter2/sample/matchers"  //对包做初始化操作
	"github.com/goinaction/code/chapter2/sample/search"
)

一个包定义一组编译过的代码,包的名字类似命名空间,可以用来间接访问包内声明的标识符。
Go 编译器不允许声明导入某个包却不使用。下划线让编译器接受这类导入

// init is called prior to main.
func init() {
	// Change the device for logging to stdout.
	log.SetOutput(os.Stdout)
}

程序中每个代码文件里的 init 函数都会在 main 函数执行前调用,


// main is the entry point for the program.
func main() {
	// Perform the search for the specified term.
	search.Run("president")
}

这一行调用了 search 包里的 Run 函数。这个函数包含程序的核心业务逻辑

2.3 search 包

package search
// 第三方包不同,从标准库中导入代码时,只需要给出要导入的包名
import (
	"log"
	"sync"
)
// 为 Matcher 类型的映射(map),这个映射以 string 类型值作为键,Matcher类型值作为映射后的值
// A map of registered matchers for searching.
// 变量名 matchers 是以小写字母开头的
var matchers = make(map[string]Matcher)

// Run performs the search logic.
func Run(searchTerm string) {
	// Retrieve the list of feeds to search through.
	feeds, err := RetrieveFeeds()
	if err != nil {
		log.Fatal(err)
	}

	// Create an unbuffered channel to receive match results to display.
	results := make(chan *Result)

	// Setup a wait group so we can process all the feeds.
	var waitGroup sync.WaitGroup

	// Set the number of goroutines we need to wait for while
	// they process the individual feeds.
	waitGroup.Add(len(feeds))

	// Launch a goroutine for each feed to find the results.
	for _, feed := range feeds {
		// Retrieve a matcher for the search.
		matcher, exists := matchers[feed.Type]
		if !exists {
			matcher = matchers["default"]
		}

		// Launch the goroutine to perform the search.
		go func(matcher Matcher, feed *Feed) {
			Match(matcher, feed, searchTerm, results)
			waitGroup.Done()
		}(matcher, feed)
	}

	// Launch a goroutine to monitor when all the work is done.
	go func() {
		// Wait for everything to be processed.
		waitGroup.Wait()

		// Close the channel to signal to the Display
		// function that we can exit the program.
		close(results)
	}()

	// Start displaying results as they are available and
	// return after the final result is displayed.
	Display(results)
}

// Register is called to register a matcher for use by the program.
func Register(feedType string, matcher Matcher) {
	if _, exists := matchers[feedType]; exists {
		log.Fatalln(feedType, "Matcher already registered")
	}

	log.Println("Register", feedType, "matcher")
	matchers[feedType] = matcher
}

2.3.1 search.go

// 为 Matcher 类型的映射(map),这个映射以 string 类型值作为键,Matcher类型值作为映射后的值
// A map of registered matchers for searching.
// 变量名 matchers 是以小写字母开头的
var matchers = make(map[string]Matcher)

Go 语言里,标识符要么从包里公开,要么不从包里公开

当代码导入了一个包时,程序可以直接访问这个包中任意一个公开的标识符,。这些标识符以大写字母开头

以小写字母开头的标识符是不公开的,不能被其他包中的代码直接访问。但是,其他包可以间接访问不公开的标识符。例如,一个函数可以返回一个未公开类型的值,那么这个函数的任何调用者,哪怕调用者不是在这个包里声明的,都可以访问这个值。

使用赋值运算符和特殊的内置函数 make 初始化了变量

map 是 Go 语言里的一个引用类型,需要使用 make 来构造,如果不先构造 map 并将构造后的值赋值给变量,直接使用会报错

map 变量默认的零值是 nil

func Run(searchTerm string) {
	// Go 语言使用关键字 func 声明函数,关键字后面紧跟着函数名、参数以及返回值
	// 读取文件
	feeds, err := RetrieveFeeds()
  //函数返回两个值。第一个返回值是一组 Feed 类型的切片,第二个返回值是一个错误值
	if err != nil {
		log.Fatal(err)
	}

	//创建一个无缓冲的通道,接收匹配后的结果
	results := make(chan *Result)
  // 在 Go 语言中,通道(channel)和映射(map)与切片(slice)一样,也是引用类型
	//  构造一个 wait group,以便处理所有的数据源
	var waitGroup sync.WaitGroup
  // 使用 sync 包的 WaitGroup 跟踪所有启动的 goroutine

  // WaitGroup 变量的值设置为将要启动的 goroutine 的数量
	waitGroup.Add(len(feeds))

	// 为每个数据源启动 goroutine ,使用关键字 for range 对 feeds 切片做迭代
	for _, feed := range feeds {  // 下划线标识符的作用是占位符
		// 使用 for range 迭代切片时,每次迭代会返回两个值。第一个值是迭代的元素在切片里的索引位置,第二个值是元素值的一个副本。
		matcher, exists := matchers[feed.Type]
		if !exists {
      //如果这个键不存在,map会返回其值类型的零值作为返回值,如果这个键存在,map会返回键所对应值的副本。
			matcher = matchers["default"]
		}

		// 一个 goroutine 是一个独立于其他函数运行的函数。使用关键字 go 启动一个 goroutine,并对这个 goroutine 做并发调度
		go func(matcher Matcher, feed *Feed) {
			// 调用一个叫 Match 的函数,Match 函数的参数是一个 Matcher 类型的值、一个指向 Feed 类型值的指针
			Match(matcher, feed, searchTerm, results)
			// 递减 WaitGroup 的计数
			waitGroup.Done()
		}(matcher, feed)  //类似Js的匿名函数
	}

	// 启动一个 goroutine 来监控是否所有的工作都做完了
	go func() {
		// 等候所有任务完成
		waitGroup.Wait()

		// 用关闭通道的方式,通知 Display 函数
		// 可以退出程序了
		close(results)
	}()

	// 启动函数,显示返回的结果,
	// 并且在最后一个结果显示完后返回
	Display(results)
}

[ := ] : 简化变量声明运算符,于声明一个变量,同时给这个变量赋予初始值

在Go语言中,如果main函数返回,整个程序也就终止了。Go程序终止时,还会关闭所有之前启动且还在运行的goroutine。写并发程序的时候,最佳做法是,在main函数返回前,清理并终止所有之前启动的goroutine。编写启动和终止时的状态都很清晰的程序,有助减少bug,防止资源异常

WaitGroup 是一个计数信号量,我们可以利用它来统计所有的goroutine 是不是都完成了工作

如果要调用的函数返回多个值,而又不需要其中的某个值,就可以使用下划线标识符将其忽略

查找map里的键时,有两个选择:

  • 要么赋值给一个变量,
  • 要么为了精确查找,赋值给两个变量。赋值给两个变量时第一个值和赋值给一个变量时的值一样,是map查找的结果值。如果指定了第二个值,就会返回一个布尔标志,来表示查找的键是否存在于map里。

一个 goroutine 是一个独立于其他函数运行的函数。使用关键字 go 启动一个 goroutine,并对这个 goroutine 做并发调度

在 Go 语言中,所有的变量都以值的方式传递。因为指针变量的值是所指向的内存地址,在函数间传递指针变量,是在传递这个地址值,所以依旧被看作以值的方式在传递。

WaitGroup 的值没有作为参数传入匿名函数,但是匿名函数依旧访问到了这个值。Go 语言支持闭包,这里就应用了闭包

2.3.2 feed.go

package search

import (
	"encoding/json"  // json 包提供编解码 JSON 的功能
	"os"  //  os 包提供访问操作系统
)
// 们声明了一个叫作 dataFile 的常量
const dataFile = "data/data.json"

// 声明了一个名叫 Feed 的结构类型
type Feed struct {
	Name string `json:"site"`  // 引号里的部分被称作标记(tag)
	URI  string `json:"link"`
	Type string `json:"type"`
}

// []*Feed, error 定义了返回值
func RetrieveFeeds() ([]*Feed, error) {
	// Open the file.
	file, err := os.Open(dataFile)
	if err != nil {
		return nil, err
	}

    // 关键字 defer 会安排随后的函数调用在函数返回时才执行,回调函数,类似的python的装饰器
	defer file.Close()

	// Decode the file into a slice of pointers
	// to Feed values.
	var feeds []*Feed
	err = json.NewDecoder(file).Decode(&feeds)

	return feeds, err
}

data.json

{
	"site" : "npr",
	"link" : "http://www.npr.org/rss/rss.php?id=1001",
	"type" : "rss"
}

关键字 defer会安排随后的函数调用在函数返回时才执行。在使用完文件后,需要主动关闭文件。

使用关键字 defer 来安排调用 Close 方法,可以保证这个函数一定会被调用,哪怕函数意外崩溃终止,也能保证关键字 defer 安排调用的函数会被执行。关键字 defer 可以缩短打开文件和关闭文件之间间隔的代码行数,有助提高代码可读性,减少错误。

因为 Go 编译器可以根据赋值运算符右边的值来推导类型,声明常量的时候不需要指定类型,此外,常量的名称使用小写字母开头,表示它只能在 search包内的代码里直接访问,而不暴露到包外面

2.3.3 match.go/default.go

package search

import (
	"log"
)

// 声明返回对象结构体
type Result struct {
	Field   string
	Content string
}

// 声明了一个名为 Matcher 的接口类型
type Matcher interface {
	// Matcher 定义了要实现的新搜索类型的行为
	Search(feed *Feed, searchTerm string) ([]*Result, error)
}

// Match 函数,为每个数据源单独启动 goroutine 来执行这个函数
// 并发地执行搜索
func Match(matcher Matcher, feed *Feed, searchTerm string, results chan<- *Result) {
	// 对特定的匹配器执行搜索
	searchResults, err := matcher.Search(feed, searchTerm)
	if err != nil {
		log.Println(err)
		return
	}

	// 将结果写入通道
	for _, result := range searchResults {
		results <- result
	}
}

// Display 从每个单独的 goroutine 接收到结果后
// 在终端窗口输出
func Display(results chan *Result) {
	// 通道会一直被阻塞,直到有结果写入
	// 一旦通道被关闭,for 循环就会终止
	for result := range results {
		log.Printf("%s:\n%s\n\n", result.Field, result.Content)
	}
}

命名接口的时候,也需要遵守Go 语言命名惯例。如果接口类型包含一个方法,那么这个类型的名字以 er 结尾。如果接口类型内部声明了多个方法,其名字需要与其行为关联

如果要让一个用户定义的类型实现一个接口,这个用户定义的类型要实现接口类型里声明的所有方法,default.go文件

一旦通道关闭,goroutine 就会终止,不再工作。

一旦编译器发现 init 函数,它就会给这个函数优先执行的权限,保证其在 main 函数之前被调用。

package search

// defaultMatcher 实现了默认匹配器
type defaultMatcher struct{}
//使用一个空结构声明了一个名叫 defaultMatcher 的结构类型。空结构在创建实例时,不会分配任何内存
// init 函数将默认匹配器注册到程序里
func init() {
	//  声明一个 defaultMatcher 类型的值
	var matcher defaultMatcher
	Register("default", matcher)
}

// / Search 实现了默认匹配器的行为,方法声明为使用 defaultMatcher 类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string) ([]*Result, error) {
	return nil, nil
}

如果声明函数的时候带有接收者,则意味着声明了一个方法。这个方法会和指定的接收者的类型绑在一起.

// 方法声明为使用 defaultMatcher 类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 声明一个指向 defaultMatcher 类型值的指针
dm := new(defaultMatch)
// 编译器会解开 dm 指针的引用,使用对应的值调用方法
dm.Search(feed, "test")
// 方法声明为使用指向 defaultMatcher 类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 声明一个 defaultMatcher 类型的值
var dm defaultMatch
// 编译器会自动生成指针引用 dm 值,使用指针调用方法
dm.Search(feed, "test")

2.4 RSS 匹配器

package matchers

import (
	"encoding/xml"
	"errors"
	"fmt"
	"log"
	"net/http"
	"regexp"

	"github.com/goinaction/code/chapter2/sample/search"
)

type (
	//  item 根据 item 字段的标签,将定义的字段
	// 与 rss 文档的字段关联起来
	item struct {
		XMLName     xml.Name `xml:"item"`
		PubDate     string   `xml:"pubDate"`
		Title       string   `xml:"title"`
		Description string   `xml:"description"`
		Link        string   `xml:"link"`
		GUID        string   `xml:"guid"`
		GeoRssPoint string   `xml:"georss:point"`
	}

	// image defines the fields associated with the image tag
	// in the rss document.
	image struct {
		XMLName xml.Name `xml:"image"`
		URL     string   `xml:"url"`
		Title   string   `xml:"title"`
		Link    string   `xml:"link"`
	}

	// channel defines the fields associated with the channel tag
	// in the rss document.
	channel struct {
		XMLName        xml.Name `xml:"channel"`
		Title          string   `xml:"title"`
		Description    string   `xml:"description"`
		Link           string   `xml:"link"`
		PubDate        string   `xml:"pubDate"`
		LastBuildDate  string   `xml:"lastBuildDate"`
		TTL            string   `xml:"ttl"`
		Language       string   `xml:"language"`
		ManagingEditor string   `xml:"managingEditor"`
		WebMaster      string   `xml:"webMaster"`
		Image          image    `xml:"image"`
		Item           []item   `xml:"item"`
	}

	// rssDocument defines the fields associated with the rss document.
	rssDocument struct {
		XMLName xml.Name `xml:"rss"`
		Channel channel  `xml:"channel"`
	}
)

// rssMatcher 实现了 Matcher 接口
type rssMatcher struct{}

// init registers the matcher with the program.
func init() {
	var matcher rssMatcher
	search.Register("rss", matcher)
}

// Search 在文档中查找特定的搜索项
func (m rssMatcher) Search(feed *search.Feed, searchTerm string) ([]*search.Result, error) {
	var results []*search.Result

	log.Printf("Search Feed Type[%s] Site[%s] For URI[%s]\n", feed.Type, feed.Name, feed.URI)

	//获取要搜索的数据
	document, err := m.retrieve(feed)
	if err != nil {
		return nil, err
	}

	for _, channelItem := range document.Channel.Item {
		// 检查标题部分是否包含搜索项
		matched, err := regexp.MatchString(searchTerm, channelItem.Title)
		if err != nil {
			return nil, err
		}

		//如果找到匹配的项,将其作为结果保存
		if matched {
			results = append(results, &search.Result{
				Field:   "Title",
				Content: channelItem.Title,
			})
		}

		// 检查描述部分是否包含搜索项
		matched, err = regexp.MatchString(searchTerm, channelItem.Description)
		if err != nil {
			return nil, err
		}

		// 如果找到匹配的项,将其作为结果保存
		if matched {
			results = append(results, &search.Result{
				Field:   "Description",
				Content: channelItem.Description,
			})
		}
	}

	return results, nil
}

//  retrieve 发送 HTTP Get 请求获取 rss 数据源并解码
func (m rssMatcher) retrieve(feed *search.Feed) (*rssDocument, error) {
	if feed.URI == "" {
		return nil, errors.New("No rss feed uri provided")
	}

	// 从网络获得 rss 数据源文档
	resp, err := http.Get(feed.URI)
	if err != nil {
		return nil, err
	}

	// 一旦从函数返回,关闭返回的响应链接
	defer resp.Body.Close()

	// 检查状态码是不是 200,这样就能知道
	// proper response.
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("HTTP Response Error %d\n", resp.StatusCode)
	}

	// 将 rss 数据源文档解码到我们定义的结构类型里
	// We don't need to check for errors, the caller can do this.
	var document rssDocument
	err = xml.NewDecoder(resp.Body).Decode(&document)
	return &document, err
}

第 3 章 打包和工具链

3.1 包

所有 Go 语言的程序都会组织成若干组文件,每组文件被称为一个包。这样每个包的代码都可以作为很小的复用单元,被其他项目引用

所有的.go 文件,除了空行和注释,都应该在第一行声明自己所属的包。

3.1.1 包名惯例

给包命名的惯例是使用包所在目录的名字。这让用户在导入包的时候,就能清晰地知道包名。

给包及其目录命名时,应该使用简洁、清晰且全小写的名字,这有利于开发时频繁输入包名

3.1.2 main 包

在 Go 语言里,命名为 main 的包具有特殊的含义。Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。

package main

import "fmt"

func main(){
	fmt.Print("你好,世界")
}

3.2 导入

编译器会使用 Go 环境变量设置的路径,通过引入的相对路径来查找磁盘上的包。

标准库中的包会在安装 Go 的位置找到。

Go 开发者创建的包会在 GOPATH 环境变量指定的目录里查找。
GOPATH 指定的这些目录就是开发者的个人工作空间。

3.2.1 远程导入

go get 将获取任意指定的 URL 的包,或者一个已经导入的包所依赖的其他包。由于 go get 的这种递归特性,这个命令会扫描某个包的源码树,获取能找到的所有依赖包。

3.2.2 命名导入

命名导入是指,在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。

当你导入了一个不在代码里使用的包时,Go 编译器会编译失败,并输出一个错误

有时,用户可能需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_来重命名这个导入

3.3 函数 init

每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。

所有被编译器发现的init 函数都会安排在main函数之前执行。

init函数用在设置包初始化变量或者其他要在程序运行前优先完成的引导工作

package postgres

import (
	"database/sql"
	"database/sql/driver"
	"errors"
)

// PostgresDriver provides our implementation for the
// sql package.
type PostgresDriver struct{}

// Open provides a connection to the database.
func (dr PostgresDriver) Open(string) (driver.Conn, error) {
	return nil, errors.New("Unimplemented")
}

var d *PostgresDriver

// 创建一个 postgres 驱动的实例。
func init() {
	d = new(PostgresDriver)
	// init 函数会将自身注册到 sql 包里
	sql.Register("postgres", d)
}

新的数据库驱动写程序时,我们使用空白标识符来导入包以让 init函数发现并被调度运行,让编译器不会因为包未被使用而产生错误。

package main

import (
	"database/sql"

	_ "github.com/goinaction/code/chapter3/dbdriver/postgres"
)

// main is the entry point for the application.
func main() {
	sql.Open("postgres", "mydb")
}

3.4 使用 Go 的工具

创建一个go文件,hello.go

┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ vim hello.go
package main

import (
        "fmt"
)
func main() {
        fmt.Println("你好,世界")
}

go run命令会先构建 hello.go 里包含的程序,然后执行构建后的程序

┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ go run hello.go
你好,世界

go build编译生成二进制文件文件,可以直接运行,感觉这里比python方便很多

┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ ls
hello.go
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ go build hello.go
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ ls
hello  hello.go
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ ./hello
你好,世界

go clean命令,调用clean后会删除编译生成的可执行文件

┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ go clean  hello.go
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ ls
hello.go
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$

3.5 进一步介绍 Go 开发工具

┌──[root@liruilongs.github.io]-[/]
└─$ go
Go is a tool for managing Go source code.

Usage:

        go <command> [arguments]

The commands are:

        bug         start a bug report
        build       compile packages and dependencies
        clean       remove object files and cached files
        doc         show documentation for package or symbol
        env         print Go environment information
        fix         update packages to use new APIs
        fmt         gofmt (reformat) package sources
        generate    generate Go files by processing source
        get         add dependencies to current module and install them
        install     compile and install packages and dependencies
        list        list packages or modules
        mod         module maintenance
        run         compile and run Go program
        test        test packages
        tool        run specified go tool
        version     print Go version
        vet         report likely mistakes in packages

3.5.1 go vet

vet 命令会帮开发人员检测代码的常见错误。

  • Printf 类函数调用时,类型匹配错误的参数。
  • 定义常用的方法时,方法签名的错误。
  • 错误的结构标签。
  • 没有指定字段名的结构字面量。
package main

import "fmt"

func main(){
   fmt.Print("输出一个没有格式化的浮点数",3.1
}
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ go vet ./demo_vet.go
# command-line-arguments
vet: ./demo_vet.go:6:59: missing ',' before newline in argument list (and 1 more errors)
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$

3.5.2 Go 代码格式化

package main

import ("fmt")

func main() {fmt.Println("你好,世界")}
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ go fmt hello.go
hello.go
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ cat hello.go
package main

import (
        "fmt"
)

func main() { fmt.Println("你好,世界") }
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$

3.5.3 Go 语言的文档

┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ go doc fmt | grep -i -A 10  examples
period with no following number specifies a precision of zero. Examples:

    %f     default width, default precision
    %9f    width 9, default precision
    %.2f   default width, precision 2
    %9.2f  width 9, precision 2
    %9.f   width 9, precision 0

Width and precision are measured in units of Unicode code points, that is,
runes. (This differs from C''s printf where the units are always measured in
bytes.) Either or both of the flags may be replaced with the character '*',
--
these examples:

    Wrong type or unknown verb: %!verb(type=value)
        Printf("%d", "hi"):        %!d(string=hi)
    Too many arguments: %!(EXTRA type=value)
        Printf("hi", "guys"):      hi%!(EXTRA string=guys)
    Too few arguments: %!verb(MISSING)
        Printf("hi%d"):            hi%!d(MISSING)
    Non-int for width or precision: %!(BADWIDTH) or %!(BADPREC)
        Printf("%*s", 4.5, "hi"):  %!(BADWIDTH)hi
        Printf("%.*s", 4.5, "hi"): %!(BADPREC)hi
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$

3.6 与其他Go开发者合作

以分享为目的创建代码库
1.包应该在代码库的根目录中
2.包可以非常小
3.对代码执行 go fmt
4.给代码写文档

3.7 依赖管理

最流行的依赖管理工具是

  • Keith Rarik :godep
  • Daniel Theophanes :vender
  • Gustavo Niemeyer : gopkg.in

3.7.1 第三方依赖

像 godep 和 vender 这种社区工具已经使用第三方(verdoring)导入路径重写这种特性解决了
依赖问题。其思想是把所有的依赖包复制到工程代码库中的目录里,然后使用工程内部的依赖包
所在目录来重写所有的导入路径。

3.7.2 对 gb 的介绍

gb 是一个由 Go 社区成员开发的全新的构建工具

gb 既不包装 Go 工具链,也不使用 GOPATH。gb 基于工程将 Go 工具链工作空间的元信息做替换。这种依赖管理的方法不需要重写工程内代码的导入路径。而且导入路径依旧通过 go get 和 GOPATH 工作空间来管理。

第 4 章 数组、切片和映射

Go 语言有 3 种数据结构可以让用户管理集合数据:数组、切片和映射

4.1 数组的内部实现和基础功能

数组是切片和映射的基础数据结构

4.1.1 内部实现

在 Go 语言里,数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。

数组因为其占用的内存是连续分配的。CPU能把正在使用的数据缓存更久的时间。而且内存连续很容易计算索引,可以快速迭代数组里的所有元素。数组的类型信息可以提供每次访问一个元素时需要在内存中移动的距离。

既然数组的每个元素类型相同,又是连续分配,就可以以固定速度索引数组中的任意数据,速度非常快。

4.1.2 声明和初始化

声明数组时需要指定内部存储的数据的类型,以及需要存储的元素的数量

  • 声明一个数组,并设置为零值
var arrays [5]int
  • 使用数组字面量声明数组
arrays := [5]int{10,12,13}
  • 让 Go 自动计算声明数组的长度
array := [...]int{10, 20,30, 40, 50}
  • 声明数组并指定特定元素的值,用具体值初始化索引为 1 和 2 的元素
array := [5]int{1: 10, 2: 20}

4.1.3 使用数组

内存布局是连续的,所以数组是效率很高的数据结构,在访问数组里任意元素的时候,使用[]运算符

  • 访问数组元素
//声明一个包含 5 个元素的整型数组
array := [5]int{10, 20, 30, 40, 50}
// 修改索引为 2 的元素的值
array[2] = 35

声明一个所有元素都是指针的数组。使用*运算符就可以访问元素指针所指向的值

  • 访问指针数组的元素
// 声明包含 5 个元素的指向整数的数组
// 用整型指针初始化索引为 0 和 1 的数组元素
array := [5]*int{0: new(int), 1: new(int)}
// 为索引为 0 和 1 的元素赋值
*array[0] = 10
*array[1] = 20

在 Go 语言里,数组是一个值。这意味着数组可以用在赋值操作中。变量名代表整个数组,同样类型的数组可以赋值给另一个数组

// 声明第一个包含 5 个元素的字符串数组
var array1 [5]string
// 声明第二个包含 5 个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 把 array2 的值复制到 array1
array1 = array2
  • 编译器会阻止类型不同的数组互相赋值
package main

import "fmt"

func main() {
        fmt.Println("你好,世界")
        // 声明第一个包含 4 个元素的字符串数组
        var array1 [4]string

        // 声明第二个包含 5 个元素的字符串数组
        // 使用颜色初始化数组
        array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}

        // 将 array2 复制给 array1
        array1 = array2
}

go vet 检查

┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ go fmt array.go
array.go
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ vim array.go
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$ go vet array.go
# command-line-arguments
vet: ./array.go:15:11: cannot use array2 (variable of type [5]string) as [4]string value in assignment
┌──[root@liruilongs.github.io]-[/usr/local/go/src/demo]
└─$

4.1.4 多维数组

数组本身只有一个维度,不过可以组合多个数组创建多维数组。多维数组很容易管理具有父子关系的数据或者与坐标系相关联的数据

  • 声明二维数组
// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为 1 个和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}
  • 访问二维数组的元素
// 声明一个 2×2 的二维整型数组
var array [2][2]int
// 设置每个元素的整型值
array[0][0] = 10

只要类型一致,就可以将多维数组互相赋值

// 声明两个不同的二维整型数组
var array1 [2][2]int
var array2 [2][2]int
// 为每个元素赋值
array2[0][0] = 10
array2[0][1] = 20
array2[1][0] = 30
array2[1][1] = 40
  • 同样类型的多维数组赋值
// 将 array2 的值复制给 array1
array1 = array2
  • 使用索引为多维数组赋值
// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将外层数组的索引为 1、内层数组的索引为 0 的整型值复制到新的整型变量里
var value int = array1[1][0]

4.1.5 在函数间传递数组

根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数之间传递变量时,总是以值的方式传递的。如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。

  • 使用值传递,在函数间传递大数组
// 声明一个需要 8 MB 的数组,创建一个包含 100 万个 int 类型元素的数组
var array [1e6]int
// 将数组传递给函数 foo
foo(array)
// 函数 foo 接受一个 100 万个整型值的数组
func foo(array [1e6]int) {
...
} 

每次函数foo被调用时,必须在栈上分配8 MB的内存

还有一种更好且更有效的方法来处理这个操作。可以只传入指向数组的指针,这样只需要复制8字节的数据而不是8 MB 的内存数据到栈上

// 分配一个需要 8 MB 的数组
var array [1e6]int
// 将数组的地址传递给函数 foo
foo(&array)
// 函数 foo 接受一个指向 100 万个整型值的数组的指针
func foo(array *[1e6]int) {
...
}

将数组的地址传入函数,只需要在栈上分配 8 字节的内存给指针就可以,这个操作会更有效地利用内存,性能也更好。不过要意识到,因为现在传递的是指针,所以如果改变指针指向的值,会改变共享的内存

4.2 切片的内部实现和基础功能

切片是一种数据结构(类似于JavaArrayList),围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。

因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引迭代以及为垃圾回收优化的好处。

4.2.1 内部实现

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有3个字段的数据结构,这些数据结构包含Go语言需要操作底层数组的元数据

  • 指向底层数组的指针
  • 切片访问的元素的个数(即长度)
  • 切片允许增长,到的元素个数(即容量)

在这里插入图片描述

4.2.2 创建和初始化

Go语言中有几种方法可以创建和初始化切片。是否能提前知道切片需要的容量通常会决定要如何创建切片

1.make 和切片字面量

  • 如果只指定长度,那么切片的容量和长度相等
	// 其长度和容量都是 5 个元素
	slice := make([]string, 5)
  • 使用长度和容量声明整型切片
func main() {
	// 其长度和容量都是 5 个元素
	slice := make([]int, 3, 5)
	fmt.Println(slice)
}
============
[Running] go run "d:\GolandProjects\code-master\demo\make.go"
[0 0 0]

剩余的 2 个元素可以在后期操作中合并到切片,如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多余容量的元素。

  • 不允许创建容量小于长度的切片,
func main() {
	// 其长度和容量都是 5 个元素
	slice := make([]int, 5, 3)
	fmt.Println(slice)
}
=================
[Running] go run "d:\GolandProjects\code-master\demo\make.go"
# command-line-arguments
d:\GolandProjects\code-master\demo\make.go:10:15: len larger than cap in make([]int)

另一种常用的创建切片的方法是使用切片字面量,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定

  • 通过切片字面量来声明切片
slice:= [] string{"Red", "Blue", "Green", "Yellow", "Pink"}
//其长度和容量都是 3 个元素
slice := []int{10, 20, 30}

当使用切片字面量时,可以设置初始长度和容量,创建长度和容量都是 100 个元素的切片

  • 使用索引声明切片
// 使用空字符串初始化第 100 个元素
slice := []string{99: ""}
  • 声明数组和声明切片的不同
// 创建有 3 个元素的整型数组
array := [3]int{10, 20, 30}
// 创建长度和容量都是 3 的整型切片
slice := []int{10, 20, 30}

2.nil 和空切片

  • 创建 nil 切片:描述一个不存在的切片时
// 创建 nil 整型切片
var slice []int

在这里插入图片描述

  • 声明空切片:表示空集合时空切片很有用
// 使用 make 创建空的整型切片
slice := make([]int, 0)
// 使用切片字面量创建空的整型切片
slice := []int{}

在这里插入图片描述
不管是使用 nil 切片还是空切片,对其调用内置函数 append、len 和 cap 的效果都是一样的。

4.2.3 使用切片

1.赋值和切片

对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一样。使用[]操作符就可以改变某个元素的值

  • 使用切片字面量来声明切片
// 其容量和长度都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 改变索引为 1 的元素的值
slice[1] = 25

切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分

  • 使用切片创建切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}

  • 使用切片创建切片,如何计算长度和容量
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]

对底层数组容量是k的切片 slice[i:j]来说

  • 长度: j - i = 2
  • 容量: k - i = 4

这里书里讲的个人感觉不太好理解,其实类似Java中String的subString,,换句话讲,前开后闭(即前包后不包),切取原数组索引1到3的元素,这里的元素个数即为新的切片长度,切取的容量为原数组第一个切点到数组末尾。

在这里插入图片描述

我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分,这个和java里的List方法subList特别像,都是通控制索引来对底层数组进行切片,所以本质上,切片后的数组可以看做是原数组的视图。

  • 修改切片内容可能导致的结果
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 其长度是 2 个元素,容量是 4 个元素
newSlice := slice[1:3]
// 修改 newSlice 索引为 1 的元素
// 同时也修改了原来的 slice 的索引为 2 的元素
newSlice[1] = 35

  • 表示索引越界的语言运行时错误
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
// 修改 newSlice 索引为 3 的元素
// 这个元素对于 newSlice 来说并不存在
newSlice[3] = 45

2.切片增长
相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量。Go语言内置的 append函数会处理增加长度时的所有操作细节。

函数append总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量

  • 使用 append 向切片增加元素
package main

import (
	"fmt"
)

func main() {
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
fmt.Println(newSlice)
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice = append(newSlice, 60)
fmt.Println(newSlice)
}
[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
[20 30]
[20 30 60]

[Done] exited with code=0 in 1.28 seconds

在这里插入图片描述

如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值

package main

import (
	"fmt"
)

func main() {
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice := append(slice, 60)
fmt.Println(newSlice)
}
[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
[10 20 30 40 50 60]

[Done] exited with code=0 in 1.236 seconds

函数append会智能地处理底层数组的容量增长。在切片的容量小于1000个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为1.25,也就是会每次增加 25%的容量。随着语言的演化,这种增长算法可能会有所改变。

3.创建切片时的 3 个索引

通过第三个索引值设置容量,如果没有第三个索引值,默认容量是到数组最后一个。

package main

import (
	"fmt"
)

func main() {
	// 创建字符串切片
	// 其长度和容量都是 5 个元素
	source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
	// 将第三个元素切片,并限制容量
	// 其长度为 1 个元素,容量为 2 个元素
	slice := source[2:3:4]
	fmt.Println(slice)
}

为了设置容量,从索引位置 2 开始,加上希望容量中包含的元素的个数(2),就得到了第三个值 4。

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
[Plum]

[Done] exited with code=0 in 0.998 seconds
  • 设置容量大于已有容量的语言运行时错误
[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
panic: runtime error: slice bounds out of range [::9] with capacity 5

如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改.

  • 设置长度和容量一样的好处
package main

import (
	"fmt"
)

func main() {
	// 创建字符串切片
	// 其长度和容量都是 5 个元素
	source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
	// 将第三个元素切片,并限制容量
	// 其长度为 1 个元素,容量为 1 个元素
	slice := source[2:3:3]
	// 向 slice 追加新字符串
	slice = append(slice, "Kiwi")
	fmt.Println(slice)
}

通过设置长度和容量一样,之后对数组的append操作都是复制原有元素新建的数组,实现了和原来数组完全隔离。

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
[Plum Kiwi]

[Done] exited with code=0 in 1.286 seconds

内置函数 append 也是一个可变参数的函数,如果使用…运算符,可以将一个切片的所有元素追加到另一个切片里

package main

import (
	"fmt"
)

func main() {
	// 创建两个切片,并分别用两个整数进行初始化
	s1 := []int{1, 2}
	s2 := []int{3, 4}
	// 将两个切片追加在一起,并显示结果
	fmt.Printf("%v\n", append(s1, s2...))
}

使用 Printf 时用来显示 append 函数返回的新切片的值

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
[1 2 3 4]

[Done] exited with code=0 in 1.472 second

4.迭代切片
既然切片是一个集合,可以迭代其中的元素。Go语言有个特殊的关键字range,它可以配合关键字for来迭代切片里的元素

  • 使用 for range 迭代切片
package main

import (
	"fmt"
)

func main() {
	// 创建一个整型切片
	// 其长度和容量都是 4 个元素
	slice := []int{10, 20, 30, 40}
	// 迭代每一个元素,并显示其值
	for index, value := range slice {
		fmt.Printf("Index: %d Value: %d\n", index, value)
	}
}

当迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40

[Done] exited with code=0 in 1.543 seconds

需要强调的是,range 创建了每个元素的副本,而不是直接返回对该元素的引用

  • range 提供了每个元素的副本
  • 使用空白标识符(下划线)来忽略索引值
for _, value := range slice {
fmt.Printf("Value: %d\n", value)
}
  • 使用传统的 for 循环对切片进行迭代
package main

import (
	"fmt"
)

func main() {
	// 创建一个整型切片
	// 其长度和容量都是 4 个元素
	slice := []int{10, 20, 30, 40}
	// 迭代每一个元素,并显示其值
	for index := 2; index < len(slice); index++ {
		fmt.Printf("Index: %d Value: %d\n", index, slice[index])
		}
}

有两个特殊的内置函数 len 和 cap,可以用于处理数组、切片和通道。对于切片,函数 len返回切片的长度

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
Index: 2 Value: 30
Index: 3 Value: 40

[Done] exited with code=0 in 1.235 seconds

函数 cap 返回切片的容量

package main

import (
	"fmt"
)

func main() {
	// 创建一个整型切片
	// 其长度和容量都是 4 个元素
	slice := []int{10, 20, 30, 40}
	// 迭代每一个元素,并显示其值
	for index := cap(slice)-1; index >= 0; index-- {
		fmt.Printf("Index: %d Value: %d\n", index, slice[index])
		}
}
========================
[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
Index: 3 Value: 40
Index: 2 Value: 30
Index: 1 Value: 20
Index: 0 Value: 10

[Done] exited with code=0 in 1.372 seconds

4.2.4 多维切片

  • 声明多维切片
// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}
  • 组合切片的切片
package main

import (
	"fmt"
)

func main() {
	// 创建一个整型切片的切片
	slice := [][]int{{10}, {100, 200}}
	// 为第一个切片追加值为 20 的元素
	slice[0] = append(slice[0], 20)
	fmt.Print(slice)
}

Go 语言里使用 append 函数处理追加的方式很简明:先增长切片,再将新的整型切片赋值给外层切片的第一个元素

[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
[[10 20] [100 200]]
[Done] exited with code=0 in 1.451 seconds

4.2.5 在函数间传递切片

在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低。让我们创建一个大切片,并将这个切片以值的方式传递给函数 foo,

// 分配包含 100 万个整型值的切片
slice := make([]int, 1e6)
// 将 slice 传递到函数 foo
slice = foo(slice)
// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
} 

在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量字段分别需要 8 字节由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组

在这里插入图片描述

在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本。

4.3 映射的内部实现和基础功能

映射是一种数据结构,用于存储一系列无序的键值对。对比java里的Map,python里的字典,也可以理解为以哈希值做索引,期望索引可以在一定的范围内的数组。

映射里基于键来存储值。映射功能强大的地方是,能够基于键快速检索数据。键就像索引一样,指向与该键关联的值。

4.3.1 内部实现

映射是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素。但映射是无序的集合,意味着没有办法预测键值对被返回的顺序。即便使用同样的顺序保存键值对,每次迭代映射的时候顺序也可能不一样。无序的原因是映射的实现使用了散列表

4.3.2 创建和初始化

Go 语言中有很多种方法可以创建并初始化映射,可以使用内置的 make 函数,也可以使用映射字面量。

package main

import (
	"fmt"
)

func main() {
	// 创建一个映射,键的类型是 string,值的类型是 int
	dict := make(map[string]int)
	// 创建一个映射,键和值的类型都是 string
	// 使用两个键值对初始化映射
	dict_ := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
	fmt.Println(dict)
	fmt.Print(dict_)
}

======
map[]
map[Orange:#e95a22 Red:#da1337]

创建映射时,更常用的方法是使用映射字面量。映射的初始长度会根据初始化时指定的键值对的数量来确定。

映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,只要这个值可以使用==运算符做比较

声明一个存储字符串切片的映射

// 创建一个映射,使用字符串切片作为值
dict := map[int][]string{}

4.3.3 使用映射

键值对赋值给映射,是通过指定适当类型的键并给这个键赋一个值来完成的

  • 为映射赋值
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将 Red 的代码加入到映射
colors["Red"] = "#da1337"
  • 可以通过声明一个未初始化的映射来创建一个值为 nil 的映射

从映射取值时有两个选择。第一个选择是,可以同时获得值,以及一个表示这个键是否存在的标志,

  • 从映射获取值并判断键是否存在
// 获取键 Blue 对应的值
value, exists := colors["Blue"]
// 这个键存在吗?
if exists {
fmt.Println(value)
} 

另一个选择是,只返回键对应的值,然后通过判断这个值是不是零值来确定键是否存在

  • 从映射获取值,并通过该值判断键是否存在
// 获取键 Blue 对应的值
value := colors["Blue"]
// 这个键存在吗?
if value != "" {
fmt.Println(value)
} 

在 Go 语言里,通过键来索引映射时,即便这个键不存在也总会返回一个值。在这种情况下,返回的是该值对应的类型的零值

  • 使用 range 迭代映射
// 创建一个映射,存储颜色以及颜色对应的十六进制代码
colors := map[string]string{
"AliceBlue": "#f0f8ff",
"Coral": "#ff7F50",
"DarkGray": "#a9a9a9",
"ForestGreen": "#228b22",
}
// 显示映射里的所有颜色
for key, value := range colors {
	fmt.Printf("Key: %s Value: %s\n", key, value)
} 
  • 从映射中删除一项
// 删除键为 Coral 的键值对
delete(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
	fmt.Printf("Key: %s Value: %s\n", key, value)
} 

4.3.4 在函数间传递映射

在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改

package main

import (
	"fmt"
)

func main() {
	// 创建一个映射,存储颜色以及颜色对应的十六进制代码
	colors := map[string]string{
		"AliceBlue":   "#f0f8ff",
		"Coral":       "#ff7F50",
		"DarkGray":    "#a9a9a9",
		"ForestGreen": "#228b22",
	}
	// 显示映射里的所有颜色
	for key, value := range colors {
		fmt.Printf("Key: %s Value: %s\n", key, value)
	}
	fmt.Println("调用函数来移除指定的键")
	// 调用函数来移除指定的键
	removeColor(colors, "Coral")
	
	// 显示映射里的所有颜色
	for key, value := range colors {
		fmt.Printf("Key: %s Value: %s\n", key, value)
	}
}

// removeColor 将指定映射里的键删除
func removeColor(colors map[string]string, key string) {
	delete(colors, key)
}
[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
Key: Coral Value: #ff7F50
Key: DarkGray Value: #a9a9a9
Key: ForestGreen Value: #228b22
Key: AliceBlue Value: #f0f8ff
调用函数来移除指定的键
Key: AliceBlue Value: #f0f8ff
Key: DarkGray Value: #a9a9a9
Key: ForestGreen Value: #228b22

[Done] exited with code=0 in 1.419 seconds

第 5 章 Go 语言的类型系统

Go 语言是一种静态类型的编程语言。这意味着,编译器需要在编译时知晓程序里每个值的类型。如果提前知道类型信息,编译器就可以确保程序合理地使用值。这有助于减少潜在的内存异常和 bug,并且使编译器有机会对代码进行一些性能优化,提高执行效率。

值的类型给编译器提供两部分信息:

  • 第一部分,需要分配多少内存给这个值(即值的规模);
  • 第二部分,这段内存表示什么。

许多内置类型的情况来说,规模和表示是类型名的一部分

  • int64 类型的值需要 8 字节(64 位),表示一个整数值;
  • float32 类型的值需要 4 字节(32 位),表示一个 IEEE-754 定义的二进制浮点数;
  • bool 类型的值需要 1 字节(8 位),表示布尔值 true和 false。

5.1 用户定义的类型(结构体)

Go 语言允许用户定义类型。当用户声明一个新类型时,这个声明就给编译器提供了一个框架,告知必要的内存大小和表示信息

Go 语言里声明用户定义的类型有两种方法。最常用的方法是使用关键字struct,它可以让用户创建一个结构类型
结构里每个字段都会用一个已知类型声明。这个已知类型可以是内置类型,也可以是其他用户定义的类型。

  • 声明一个结构类型
type user struct {
	name  string
	email string
}

  • 使用结构类型声明变量,并初始化为其零值
var bill user

当声明变量时,这个变量对应的值总是会被初始化。这个值要么用指定的值初始化,要么用零值(即变量类型的默认值)做初始化

  • 对数值类型来说,零值是 0;
  • 对字符串来说,零值是空字符串;
  • 对布尔类型,零值是 false。

:=:一个短变量声明操作符在一次操作中完成两件事情:声明一个变量,并初始化

  • 使用结构字面量来声明一个结构类型的变量
lisa := user{
	name: "liruilong",
	email: "liruilong@qq.com"
}
  • 使用结构字面量创建结构类型的值
user{
	name: "liruilong",
	email: "liruilong@qq.com"	
}
  • 不使用字段名,创建结构类型的值
user{"Bill", "bill@email.com"}

这种形式下,值的顺序很重要,必须要和结构声明中字段的顺序一致。当声明结构类型时,字段的类型并不限制在内置类型,也可以使用其他用户定义的类型

  • 使用其他结构类型声明字段
type admin struct{
	liruilong user,
	leve string
}
fred := admin{
	liruilong: user{"Bill", "bill@email.com"},
	level: "super",
}

另一种声明用户定义的类型的方法是,基于一个已有的类型,将其作为新类型的类型说明。

  • 基于 int64 声明一个新类型
type Duration int64

Duration 是一种描述时间间隔的类型,单位是纳秒(ns)。这个类型使用内置的 int64 类型作为其表示

我们把 int64 类型叫作 Duration 的基础类型,Go 并不认为 Duration 和 int64 是同一种类型。这两个类型是完全不同的有区别的

  • 给不同类型的变量赋值会产生编译错误
package main

import (
	"fmt"
)
type Duration int64

func main() {
	var dur Duration
	dur = int64(1000)
	fmt.Println(dur)
}

============
[Running] go run "d:\GolandProjects\code-master\demo\hello.go"
# command-line-arguments
demo\hello.go:10:6: cannot use int64(1000) (type int64) as type Duration in assignment

[Done] exited with code=2 in 0.705 seconds

5.2 方法

方法能给用户定义的类型添加新的行为。方法实际上也是函数,只是在声明时,在关键字func 和方法名之间增加了一个参数

关键字 func 和函数名之间的参数被称作接收者,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称 为方法。

Go 语言里有两种类型的接收者:值接收者,指针接收者。

  • 使用值接收者声明一个方法
// notify 使用值接收者实现了一个方法
func (u user) notify() {
	fmt.Printf("Sending User Email To %s<%s>\n",
		u.name,
		u.email)
}

如果使用值接收者声明方法,调用时会使用这个值的一个副本来执行。使用 bill 的值作为接收者进行调用,方法 notify 会接收到 bill 的值的一个副本。

	// user 类型的值可以用来调用使用值接收者声明的方法
	bill := user{"Bill", "bill@email.com"}
	bill.notify()

也可以使用指针来调用使用值接收者声明的方法,使用指向 user 类型值的指针来调用 notify 方法

    // 指向 user 类型值的指针也可以用来调用使用值接收者声明的方法
	lisa := &user{"Lisa", "lisa@email.com"}
	lisa.notify() //(*lisa).notify()

指针被解引用为值,不管是变量调用还是指针调用,notify 操作的都是一个副本。

  • 使用指针接收者实现了一个方法
// changeEmail 使用指针接收者实现了一个方法
func (u *user) changeEmail(email string) {
	u.email = email
}

使用指针接收者声明。这个接收者的类型是指向 user 类型值的指针,而不是 user 类型的值。当调用使用指针接收者声明的
方法时,这个方法会共享调用方法时接收者所指向的值

lisa := &user{"Lisa", "lisa@email.com"}
//  指向 user 类型值的指针可以用来调用使用指针接收者声明的方法
lisa.changeEmail("lisa@newdomain.com")
lisa.notify()

值接收者使用值的副本来调用方法,而指针接受者使用实际值来调用方法。

bill := user{"Bill", "bill@email.com"}
// user 类型的值可以用来调用使用指针接收者声明的方法
bill.changeEmail("bill@newdomain.com")
bill.notify() //(&bill).changeEmail ("bill@newdomain.com")

Go语言既允许使用值,也允许使用指针来调用方法,不必严格符合接收者的类型

// 声明 并使用方法
package main

import (
	"fmt"
)

// user 在程序里定义一个用户类型
type user struct {
	name  string
	email string
}

// notify 使用值接收者实现了一个方法
func (u user) notify() {
	fmt.Printf("Sending User Email To %s<%s>\n",
		u.name,
		u.email)
}

// changeEmail 使用指针接收者实现了一个方法
func (u *user) changeEmail(email string) {
	u.email = email
}

// main 是应用程序的入口
func main() {
	// user 类型的值可以用来调用使用值接收者声明的方法
	bill := user{"Bill", "bill@email.com"}
	bill.notify()

	// 指向 user 类型值的指针也可以用来调用使用值接收者声明的方法
	lisa := &user{"Lisa", "lisa@email.com"}
	lisa.notify()

	// user 类型的值可以用来调用使用指针接收者声明的方法
	bill.changeEmail("bill@newdomain.com")
	bill.notify()

	//  指向 user 类型值的指针可以用来调用使用指针接收者声明的方法
	lisa.changeEmail("lisa@newdomain.com")
	lisa.notify()
}
================
[Running] go run "d:\GolandProjects\code-master\chapter5\listing11\tempCodeRunnerFile.go"
Sending User Email To Bill<bill@email.com>
Sending User Email To Lisa<lisa@email.com>
Sending User Email To Bill<bill@newdomain.com>
Sending User Email To Lisa<lisa@newdomain.com>

[Done] exited with code=0 in 2.288 seconds

p105

5.3 类型的本质

在声明一个新类型之后,声明一个该类型的方法之前,需要先回答一个问题:这个类型的本质是什么。

如果给这个类型增加或者删除某个值,是要创建一个新值,还是要更改当前的值?

如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。

这个答案也会影响程序内部传递这个类型的值的方式:是按值做传递,还是按指针做传递。

保持传递的一致性很重要。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。

5.3.1 内置类型

内置类型是由语言提供的一组类型,数值类型、字符串类型和布尔类型,这些类型本质上是原始的类型,因此,当对这些值进行增加或者删除的时候,会创建一个新值.

基于这个结论,当把这些类型(内置类型 )的值传递给方法或者函数时,应该传递一个对应值的副本

func Trim(s, cutset string) string {
	if s == "" || cutset == "" {
		return s
	}
	return TrimFunc(s, makeCutsetFunc(cutset))
}

标准库里 strings 包的 Trim 函数,这个函数对调用者原始的 string 值的一个副本做操作,并返回一个新的 string 值的副本。字符串(string)就像整数、浮点数和布尔值一样,本质上是一种很原始的数据值,所以在函数或方法内外传递时,要传递字符串的一份副本。

func isShellSpecialVar(c uint8) bool {
	switch c {
	case '*', '#', '$', '@', '!', '?', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
		return true
	}
	return false
}

env 包里的 isShellSpecialVar 函数。这个函数传入了一个 int8类型的值,并返回一个 bool 类型的值,这里的参数没有使用指针来共享参数的值或者返调用者传入了一个 uint8 值的副本,并接受一个返回值 true 或者 false。

5.3.2 引用类型

Go 语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型

当声明上述类型的变量时,创建的变量被称作标头(header)值。从技术细节上说,字符串也是一种引用类型。

每个引用类型创建的标头值是包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不需要共享一个引用类型的值

标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构

type IP []byte  //名为 IP 的类型,这个类型被声明为字节切片
......

当要围绕相关的内置类型或者引用类型来声明用户定义的行为时,直接基于已有类型来声明用户定义的类型会很好用。编译器只允许为命名的用户定义的类型声明方法.

func (ip IP) MarshalText() ([]byte, error) {
	if len(ip) == 0 {
		return []byte(""), nil
	}
	if len(ip) != IPv4len && len(ip) != IPv6len {
		return nil, &AddrError{Err: "invalid IP address", Addr: hexString(ip)}
	}
	return []byte(ip.String()), nil
}

MarshalText 方法是用 IP 类型的值接收者声明的。一个值接收者,正像预期的那样通过复制来传递引用,从而不需要通过指针来共享引用类型的值。这种传递方法也可以应用到函数或者方法的参数传递

func ipEmptyString(ip IP) string {
	if len(ip) == 0 {
		return ""
	}
	return ip.String()
}

ipEmptyString函数。这个函数需要传入一个IP 类型的值。调用者传入的是这个引用类型的值,而不是通过引用共享给这个函数,调用者将引用类型的值的副本传入这个函数。这种方法也适用于函数的返回值。最后要说的是,引用类型的值在其他方面像原始的数据类型的值一样对待。

5.3.3 结构类型

结构类型可以用来描述一组数据值,这组值的本质即可以是原始的,也可以是非原始的

如果决定在某些东西需要删除或者添加某个结构类型的值时该结构类型的值不应该被更改,那么需要遵守之前提到的内置类型和引用类型的规范。

type Time struct {
	// wall and ext encode the wall time seconds, wall time nanoseconds,
	// and optional monotonic clock reading in nanoseconds.
	//
	// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
	// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
	// The nanoseconds field is in the range [0, 999999999].
	// If the hasMonotonic bit is 0, then the 33-bit field must be zero
	// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
	// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
	// unsigned wall seconds since Jan 1 year 1885, and ext holds a
	// signed 64-bit monotonic clock reading, nanoseconds since process start.
	wall uint64
	ext  int64

	// loc specifies the Location that should be used to
	// determine the minute, hour, month, day, and year
	// that correspond to this Time.
	// The nil location means UTC.
	// All UTC times are represented with loc==nil, never loc==&utcLoc.
	loc *Location
}

Time结构选自time 包,时间点的时间是不能修改的,看下Now 函数的实现

func now() (sec int64, nsec int32, mono int64)

func Now() Time {
	sec, nsec, mono := now()
	mono -= startNano
	sec += unixToInternal - minWall
	if uint64(sec)>>33 != 0 {
		return Time{uint64(nsec), sec + minWall, Local}
	}
	return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}

这个函数创建了一个Time类型的值,并给调用者返回了Time值的副本。这个函数没有使用指针来共享Time值。之后,让我们来看一个 Time 类型的方法

func (t Time) Add(d Duration) Time {
	dsec := int64(d / 1e9)
	nsec := t.nsec() + int32(d%1e9)
	if nsec >= 1e9 {
		dsec++
		nsec -= 1e9
	} else if nsec < 0 {
		dsec--
		nsec += 1e9
	}
	t.wall = t.wall&^nsecMask | uint64(nsec) // update nsec
	t.addSec(dsec)
	if t.wall&hasMonotonic != 0 {
		te := t.ext + int64(d)
		if d < 0 && te > t.ext || d > 0 && te < t.ext {
			// Monotonic clock reading now out of range; degrade to wall-only.
			t.stripMono()
		} else {
			t.ext = te
		}
	}
	return t
}

这个方法使用值接收者,并返回了一个新的 Time 值,该方法操作的是调用者传入的 Time 值的副本,并且给调用者返回了一个方法内的 Time 值的副本。

至于是使用返回的值替换原来的 Time 值,还是创建一个新的 Time 变量来保存结果,是由调用者决定的事情。

大多数情况下,结构类型的本质并不是原始的,而是非原始的。这种情况下,对这个类型的值做增加或者删除的操作应该更改值本身。

当需要修改值本身时,在程序中其他地方,需要使用指针来共享这个值。让我们看一个由标准库中实现的具有非原始本质的结构类型的例子

\Go\src\os\types.go

//D:\Go\src\os\types.go
type File struct {
	*file // os specific
}

\Go\src\os\file_windows.go


//file 是*File 的实际表示
// 额外的一层结构保证没有哪个 os 的客户端
// 能够覆盖这些数据。如果覆盖这些数据,
// 可能在变量终结时关闭错误的文件描述符
type file struct {
	pfd        poll.FD
	name       string
	dirinfo    *dirInfo // 除了目录结构,此字段为 nil
	appendMode bool     // whether file is opened for appending
}

标准库中声明的 File 类型。这个类型的本质是非原始的,这个类型的值实际上不能安全复制。因为没有方法阻止程序员进行复制,所以File类型的实现使用了一个嵌入的指针,指向一个未公开的类型.

正是这层额外的内嵌类型阻止了复制。不是所有的结构类型都需要或者应该实现类似的额外保护。程序员需要能识别出每个类型的本质,并使用这个本质来决定如何组织类型。

Open 函数的实现

func Open(name string) (*File, error) {
	return OpenFile(name, O_RDONLY, 0)
}

调用者得到的是一个指向 File 类型值的指针。Open 创建了 File 类型的值,并返回指向这个值的指针。如果一个创建用的工厂函数返回了一个指针,就表示这个被返回的值的本质是非原始的。

即便函数或者方法没有直接改变非原始的值的状态,依旧应该使用共享的方式传递.

func (f *File)  error {
	if f == nil {
		return ErrInvalid
	}
	if e := syscall.Fchdir(f.fd); e != nil {
		return &PathError{"chdir", f.name, e}
	}
	return nil
}

这个1.17版的GO换了写法

// windows
func Chdir(dir string) error {
	if e := syscall.Chdir(dir); e != nil {
		testlog.Open(dir) // observe likely non-existent directory
		return &PathError{Op: "chdir", Path: dir, Err: e}
	}
	if log := testlog.Logger(); log != nil {
		wd, err := Getwd()
		if err == nil {
			log.Chdir(wd)
		}
	}
	return nil
}

即使没有修改接收者的值,依然是用指针接收者来声明的。因为File 类型的值具备非原始的本质,所以总是应该被共享,而不是被复制。

是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。

这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。5.4节会讲解什么是接口值,以及使用接口值调用方法的机制。

5.4 接口

多态是指代码可以根据类型的具体实现采取不同行为的能力。如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。标准库里有很好的例子,如io包里实现的流式处理接口。io包提供了一组构造得非常好的接口和函数,来让代码轻松支持流式数据处理。

只要实现两个接口,就能利用整个io包背后的所有强大能力。不过,我们的程序在声明和实现接口时会涉及很多细节。即便实现的是已有接口,也需要了解这些接口是如何工作的。

在探究接口如何工作以及实现的细节之前,我们先来看一下使用标准库里的接口的例子。

5.4.1 标准库

curl 的功能,如

//  这个示例程序展示如何使用 io.Reader 和 io.Writer 接口
//  写一个简单版本的 curl 程序
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

// init 在 main 函数之前调用
func init() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: ./example2 <url>")
		os.Exit(-1)
	}
}

// main 是应用程序的入口
func main() {
	// 从 Web 服务器得到响应
	r, err := http.Get(os.Args[1])
	if err != nil {
		fmt.Println(err)
		return
	}

	// 从 Body 复制到 Stdout
	io.Copy(os.Stdout, r.Body)
	if err := r.Body.Close(); err != nil {
		fmt.Println(err)
	}
}

http.Response 类型包含一个名为 Body 的字段,这个字段是一个 io.ReadCloser 接口类型的值

io.Copy 函数的第二个参数,接受一个 io.Reader 接口类型的值,这个值表示数据流入的源。Body 字段实现了 io.Reader接口
io.Copy 的第一个参数是复制到的目标,这个参数必须是一个实现了 io.Writer 接口,os 包里的一个特殊值 Stdout,表示标准输出设备,已经实现了 io.Writer 接口,如果学过java之类的语言这里横容易理解,类比java中IO读写,低级流包装为高级流进行交互。

┌──[root@liruilongs.github.io]-[/usr/local/go/src]
└─$ go run listing34.go
Usage: ./example2 <url>
exit status 255
┌──[root@liruilongs.github.io]-[/usr/local/go/src]
└─$ go run listing34.go  http://localhost:80
<!DOCTYPE html>
<html>
<head>
  <meta charset='utf-8' content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
  .....
// Sample program to show how a bytes.Buffer can also be used
// 用于 io.Copy 函数
package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
)

// main is the entry point for the application.
func main() {
	var b bytes.Buffer

	// 将字符串写入 Buffer
	b.Write([]byte("Hello"))

	//  使用 Fprintf 将字符串拼接到 Buffer
	fmt.Fprintf(&b, "World!")

	// 将 Buffer 的内容写到 Stdout
	io.Copy(os.Stdout, &b)
}

fmt.Fprintf 函数接受一个 io.Writer 类型的接口值作为其第一个参数,bytes.Buffer 类型的指针实现了 io.Writer 接口,bytes.Buffer 类型的指针也实现了 io.Reader 接口,再次使用 io.Copy 函数

┌──[root@liruilongs.github.io]-[/usr/local/go/src]
└─$ go run lsiting35.go
HelloWorld!

5.4.2 实现

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。

对接口值方法的调用会执行接口值里存储的用户定义的类型的值对应的方法。因为任何用户定义的类型都可以实现任何接口,所以对接口值方法的调用自然就是一种多态

在这个关系里,用户定义的类型通常叫作实体类型,原因是如果离开内部存储的用户定义的类型的值的实现,接口值并没有具体的行为。

并不是所有值都完全等同,用户定义的类型的值或者指针要满足接口的实现,需要遵守一些规则。

展示了在user类型值赋值后接口变量的值的内部布局。接口值是一个两个字长度的数据结构,

  • 第一个字包含一个指向内部表的指针。这个内部表叫作iTable,包含了所存储的值的类型信息。iTable包含了已存储的值的类型信息以及与这个值相关联的一组方法。
  • 第二个字是一个指向所存储值的指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系
    在这里插入图片描述

一个指针赋值给接口之后发生的变化。在这种情况里,类型信息会存储一个指向保存的类型的指针,而接口值第二个字依旧保存指向实体值的指针
在这里插入图片描述

5.4.3 方法集

方法集定义了接口的接受规则。

// Sample program to show how to use an interface in Go.
package main

import (
	"fmt"
)

// notifier 是一个定义了

// type behavior.
type notifier interface {
	notify()
}

// user defines a user in the program.
type user struct {
	name  string
	email string
}

// notify implements a method with a pointer receiver.
func (u *user) notify() {
	fmt.Printf("Sending user email to %s<%s>\n",
		u.name,
		u.email)
}

// main is the entry point for the application.
func main() {
	// Create a value of type User and send a notification.
	u := user{"Bill", "bill@email.com"}

	sendNotification(u)

	// ./listing36.go:32: cannot use u (type user) as type
	//                     notifier in argument to sendNotification:
	//   user does not implement notifier
	//                          (notify method has pointer receiver)
}

// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
	n.notify()
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。