image

Go语言学习笔记

  • WORDS 31942

Go语言学习笔记

官网

准备

学习 Go语言前的准备工作

下载安装

前往下载页面, 下载对应系统的安装包。

Linux或者 macOS也可以使用包管理器安装

## ArchLinux
sudo pacman -S go

## macOS
sudo brew install go

Hello World

vim helloworld.go
package main

import "fmt"

func main() {
	fmt.Println("hello world")
}

运行或者编译为可执行文件

## 直接运行
go run helloworld.go

## 编译为可执行文件并运行
go build helloworld.go

./helloworld

入门

Go语言中使用 main包来定义一个可执行程序,main包中的 main函数是程序入口函数。Go中不需要在一行代码的末尾添加 ;,除非有多个语句出现在一行,缩进采用 4空格。

package声明必须在程序的第一行,importpackage后面声明。

注释使用双斜杠 //,命名风格使用 驼峰式

关键字

  • package:包声明
  • import:导入包,
  • func:函数声明
  • var:变量声明
  • const:常量声明
  • type:结构体声明
  • &:获取一个变量的地址
  • *:引用一个指针,获取指针指向的值
  • _:表示一个忽略的变量,Go要求所有变量声明出来都是需要使用的,如果一个变量声明出来不会使用那么就是用 _关键字表示这是一个要忽略的变量

变量

Go中的变量有多种方式进行声明,并且没有 null值不存在初始化。

// 指定生成string类型的变量
var text string = "hello"
// 声明变量 通过值自动推导类型
var str = "string"
// 不使用var关键字 通过 := 声明变量并自动推导类型
num := 10

// 声明三个int类型的变量 初始值为0
var num1, num2, num3 int
// 同时声明多个变量
var num, text = 1, "hello"

第一种方式多用于类级别的变量、第三种方式多用于函数内变量、第二种比较少用

还可以使用内置的 new()函数进行声明。new()函数创建一个指定类型的零值并返回其地址

str := new(string)
// *str等于string类型的零值 空字符串 ""
fmt.Println(*str)
*str = "hello"
// *str == "hello"
fmt.Println(*str)

指针

使用 &关键字获取一个变量的地址,然后通过 *关键字获取一个指向该变量的指针。指针的零值是 nil,指针是可以使用 ==比较的,当两个指针都等于 nil或者指向同一内存地址时结果为 true

func main() {
	num := 10
	n := &num
    // num == 10, *n == 10
	fmt.Println(num, *n)
	*n = 11
    // num == 11, *n == 11
	fmt.Println(num, *n)
}

Go语言在函数传参时,传递的都是该参数的复制副本,所以在函数内修改传递的参数并不会引起外层参数的修改,并且当传递一个很大的结构体时,还可能会产生性能问题。使用指针来传递参数可以解决以上问题,指针传递的是参数的引用,不涉及到复制并且直接修改引用的值会对所有位置的参数都生效。(Java方法传参基本数据类型使用副本,包装和高级类型传递引用的副本)

func main() {
	str1 := "1"
	str2 := "2"
	testEdit(&str1)
	testEdit2(str2)
    // str1 == hello str2 == 2
	fmt.Println(str1, str2)
}

func testEdit(text *string) {
	*text = "hello"
}

func testEdit2(text string) {
	text = "hello"
}

常量

Go中的常量使用 const声明,常量可以同时声明多个并且不需要指定类型

// 声明单个
const text = "123"
// 同时声明多个常量
const (
	name = "hello"
	str  = "world"
	num  = 10
)

常量的声明还可以使用 iota,它可以创建值而无需显式写出。iota0开始,逐项递增

const (
	num1 = iota  // 0
	num2  // 1
	num3  // 2
	num4  // 3
)

使用命令行参数

使用 os.Args可以获取 Go程序运行时传递的命令行参数,多个参数使用空格间隔。os.Args[1]即第一个参数是命令本身,然后才是传递的参数。

package main

import (
    "fmt"
    "os"
)

func main() {
    // 使用切片拿到除命令本身外的所有参数
    for _, value := range os.Args[1:]{
		fmt.Println(value)
    }
}
## 传递的参数为test value
go run demo.go test value

输入

控制台输入

通过 os.Stdin来读取用户在控制台的输入

func main() {
	// 创建一个输入器
	input := bufio.NewScanner(os.Stdin)
    // 循环扫描输入
	for input.Scan() {
        // 拿到用户输入的文本
		text := input.Text()
		if text == "quit" {
			break
		}
		fmt.Println(text)
	}
}

读取文件

func main() {
    // 打开文件
	file, err := os.Open("demo.txt")
	if err != nil {
		fmt.Printf("文件打开失败,错误消息:%v\n", err)
		return
	}
	input := bufio.NewScanner(file)
    // 逐行扫描获取值
	for input.Scan() {
		value := input.Text()
		fmt.Println(value)
	}
}

格式化参数

类似于 fmt.Printf等函数,可以格式化参数的输出显示

key 描述
%d 十进制整数
%x、%o、%b 十六进制、八进制、二进制整数
%f、%g、%e 浮点数,%e以科学计数法显示
%t 布尔型
%c 字符(Unicode)码点
%s 字符串
%q 带引号的字符串
%v 内置格式的任意值
%T 任意值的类型
%% 百分号本身
// 输出10进制和2进制的 123
fmt.Printf("%d\t%b\t\n", 123, 123)

Web

发送请求

Gohttp标准库已经封装了绝大部分的功能,如果发送一个简单的 Get请求只需要调用 http.Get函数即可

func main() {
    resp, err := http.Get("https://baidu.com")
	if err != nil {
		fmt.Println("请求发送失败")
        return
	}
    // 后置语句 在方法返回后执行
	defer resp.Body.Close()
    // 输出内容到控制台
	io.Copy(os.Stdout, resp.Body)
}

稍微复杂的Post请求

func main() {
	body := "{name: \"xiao\"}"
    // 发送JSON数据的POST请求
	req, _ := http.NewRequest("https://xxxx.com", http.MethodPost, bytes.NewBufferString(body))
	req.Header.Add("Authorization", "xxx")
	req.Header.Add("Content-Type", "application/json")
	// 创建一个http客户端并发送请求
	client := &http.Client{}
	response, err := client.Do(req)
}

Web服务器

Go的标准库封装了 Web服务器相关的操作,只需要简单的代码就可以完成请求的监听和处理

func main() {
    // 针对 /hello 路径的处理 返回 "hello world"
	http.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {
		writer.Write([]byte("hello world"))
	})
    // 启动Web服务器监听8080端口
	log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

也可以使用单独的处理函数

func main() {
	http.HandleFunc("/hello", handleHello)
	log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
// 处理 /hello 路径的请求
func handleHello(writer http.ResponseWriter, req *http.Request) {
	writer.Write([]byte("hello world"))
}

程序结构

循环

Go中只有 for循环,循环可以选择一个参数,当参数为 false时,则结束循环。如果不设置参数,那么则是无限循环

// 无限循环 直到使用break关键字跳出
for {
  
}
// fori循环
for i := 0; i < 10; i++ {
    fmt.Println(i)
}
// for range循环
// range关键字会遍历参数 返回两个变量 index表示索引,value表示当前遍历的值
nums := []int{1, 2, 3, 4}
for index, value := range nums {
    fmt.Println(index, value)
}

// 可以使用 _ 忽略索引
for _, value := range nums {}

控制

Go中的控制流程使用 ifswitch关键字,操作逻辑和其他语言一致

// if
scope := 80

if scope >= 80 {
    fmt.Println("优秀")
} else if scope >= 60 {
    fmt.Println("及格")
} else {
    fmt.Println("不及格")
}

Goswitch语句会在 case块执行完毕后自动跳出,不需要显式使用 break关键字。如果需要在 case块执行完毕后继续执行下一个 case块,可以使用 fallthrough关键字

// switch
status := 0
switch status {
    case 0:
        fmt.Println("正常")
        fallthrough
    case 1:
    	fmt.Println("封禁中")
    case 2:
    	fmt.Println("账号异常")
    case 3:
    	fmt.Println("已注销")
    default:
    	fmt.Println("未知状态")
}

defer

Go中的 defer关键字用于声明需要延迟调用的函数,延迟调用发生在函数返回前。如果函数内没有返回语句或者抛出异常时延迟函数也会执行。多个延迟函数按照先进先出的顺序执行。

func main() {
	file, err := os.Open("demo.txt")
	if err != nil {
		fmt.Println("error")
		return
	}
    // 延迟调用 所有语句执行完毕后关闭资源
	defer file.Close()
	io.Copy(os.Stdout, file)
}

多重赋值

多重赋值允许多个变量一次性被赋值,当变量同时出现在赋值符两侧时特别有用

// 交换数组内元素位置的函数
func reversal(bytes []byte) []byte {
	length := len(bytes)
	for i := 0; i < length/2; i++ {
		j := length - 1 - i
        // 交换位置 使用多重赋值
		bytes[i], bytes[j] = bytes[j], bytes[i]
	}
	return bytes
}
// 使用多重赋值,拿到打开的文件或者错误消息
file, err := os.Open("xxx.txt")

包和导入

每一个 Go文件的开头使用 package声明包的名称,当导入包时使用 strings.Join()等方式引入一个包内的变量和函数。如果一个包内的变量、常量和函数需要对外导出,那么需要名称需要以大写开头。

package demo

// 不导出的函数
func reversal(){}

// 对外导出的函数 可以通过 demo.Reversal()引用
func Reversal(){}

包的初始化从初始化包级别的变量开始,也可以定义一个名称为 init的初始化函数,这个函数不能被调用和引用,会在包被引用时自动调用。同一个包内可以用多个 init函数,初始化时按照顺序执行。一个包不管被导入多少次,其中的 init函数都只会执行一次。

package demo

import "fmt"

func init() {
	fmt.Println("demo包初始化")
}

当需要导入包时,每一个包通过 导入路径的唯一标识符来标识,一个导入路径表示一个目录,目录中包含构成包的一个或多个 go文件。按照约定,包的包名通常匹配路径的最后一级目录。

当需要导入路径不同但是包名相同的两个包时,可以使用重命名导入来区分两个包。

package main

import (
    // 重命名导入 "fmt"为f
	f "fmt"
)

func main() {
	f.Println("Hello, world!")
}

Go Module

Go语言的模块化开发是 1.11版本引入的新特性,解决了 Go语言依赖管理的问题并实现了一些其它的功能:

  • 依赖管理
  • 版本控制
  • 私有仓库支持
  • 简化构建

使用

在项目根目录下使用 go mod init初始化模块

go mod init com.zeroxn/demoproject

会在项目的根目录中添加一个 go.mod文件用以管理项目的依赖关系,需要添加依赖时,使用 go get命令

go get golang.org/x/net
// go.mod

module demo

go 1.20

require (
	github.com/PuerkitoBio/goquery v1.8.1 // indirect
	github.com/andybalholm/cascadia v1.3.2 // indirect
	golang.org/x/net v0.13.0 // indirect
)

数据类型

Go的数据类型分为四大类:

  • 基础类型
  • 复合类型
  • 引用类型
  • 接口类型

对于Go中的每种类型,如果允许类型转换,那么使用 T(x)会将 x的值转换为 T类型。

基础类型

整数

Go中的整数包括有符号整数和无符号整数,分别分为4种大小:8位、16位、32位、64位,有符号整数使用 int8、int16、int32、int64表示,无符号整数使用 uint8、uint16、uint32、uint64表示

此外还有两种类型 int、uint,两种类型大小与平台相关,取决于运行的操作系统和架构。大多数情况下大小为 32位64位

rune类型是 int32类型的同义词,用于指明一个值是 Unicode码点。byte类型是 unit8类型的同义词,强调一个值是原始数据

浮点数

Go中具有两种大小的浮点数 float32float64。十进制下,float32的有效数字大约为6位,而 float64大约为15位,绝大部分情况下,都应该先使用 float64

// math包中包含了各个数字类型的最大值和最小值
fmt.Println(math.MaxFloat32)
fmt.Println(math.MaxFloat32)
fmt.Println(math.MinInt16)

浮点型转整形会舍弃小数部分(正值向下取整,负值向上取整)

num := 3.14
num2 := -3.24
// 3 -3
fmt.Println(int(num), int(num2))

复数

Go中有两种大小的复数类型:complex64complex128,分别由 float32float64组成

内置的 complex()函数根据给定的实部和虚部创建复数,内置的 real()imag()函数则分别提取复数的实部和虚部

num := complex(5, 4)
// 5 4
fmt.Println(real(num), imag(num))

或者在浮点数和十进制整数后面写字母 i,它就变成了虚数,上面的 num声明可以简化为

num := 5 + 4i

复数运算

c1 := 5 + 4i
c2 := 4 + 3i
// sum == 9 + 7i
sum := c1 + c2

布尔类型

bool类型只有两种值:truefalse!可以对布尔类型进行取反操作

bl := true
// false
fmt.Println(!bl)

字符串

字符串是不可变的字符序列,可以包含任意数据,包括0值字节

使用 len()函数可以获取字符串的长度,通过下标访问可以获取字符串中指定位置的字符。或者通过 [i:j]生成新的子串,下标从 i开始到 j结束,不包含 j

str := "hello"
// s == h
s := str[0]
// sub == hel
sub := str[0:3]

字符串可以使用 ==比较,也可以使用 =重新赋值

str1 := "hello"
str2 := "hello"
// str1 == str2 true

// 由于字符串的值无法改变 这里是将 += 生成的新字符串重新赋值给 str1
str1 += str2

// 错误 字符串无法改变 无法赋值
str1[0] = 's'

基础库

bytes操作字节类型,一些常用方法:

  • bytes.NewBuffer([]byte):通过字节切片创建 Buffer对象
  • bytes.NewBufferString(string):通过字符串创建 Buffer对象
  • bytes.Clone:克隆一个字节切片
  • bytes.Index:获取指定值的索引

strings操作字符串类型,一些常用方法:

  • strings.Trim():去除两边空格
  • strings.ReplaceAll():替换所有指定的字符串
  • strings.ToUpper():转大写
  • strings.ToLower():转小写
  • strings.Join():连接一个字符串切片
  • strings.HasPrefix():判断字符串是否以指定字符串开头
  • strings.Split():分割字符串
  • strings.Contains():判断字符串中是否包含指定字符
  • strings.Index():获取指定字符串的索引
  • strings.LastIndex():获取指定字符串的最后一个索引

strconv包提供的函数用以转换字符串、布尔值、整数和浮点数,一些常用方法:

  • strconv.FormatBool():布尔值转字符串
  • strconv.FormatInt(i int, base int):整形转字符串,i表示待转换的整数,base表示进制取值 2-32
  • strconv.FormatUint():无符号整形转字符串
  • strconv.FormatFloat():浮点数转字符串
  • strconv.ParseInt(i int, base int, bitSize int):字符串转整数。bitSize指定类型整数的大小,取值 8、16、32、34。返回转换后的整数和一个错误信息。
  • strconv.ParseUint:字符串转无符号整数
  • strconv.ParseBool():字符串转布尔型
  • strconv.ParseFloat():字符串转浮点数

unicode包提供关于判断文字值特性的函数,一些常用方法:

  • unicode.IsDigit():判断是否是数字
  • unicode.IsLetter():判断是否是字母
  • unicode.IsLower():判断是否是小写字母
  • unicode.IsUpper():判断是否是大写字母
  • unicode.IsSpace:判断是否为空白字符
  • unicode.IsControl:判断是否为控制字符
  • unicode.IsPunct:判断是否为标点符号

字符串和[]byte字节切片可以相互转换

s := "hello"
bytes := []byte(s)
ns := string(bytes)

复合类型

数组

数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。数组的长度也是数组数组类型的一部分,如果一个数组中的元素是可比较的那么这个数组也是可比较的。

// 声明具有3个长度的整型数组 默认的所有值都是4
var nums [3]int

// 使用...声明数组时 数组的长度根据所给的参数值来确定
// nums.length == 2
nums := [...] {1, 2}

// 声明一个长度100的数组 前99个元素都是零值0 最后一位值是 -1
nums := [...] {99: -1}

nums1 := [3]int{}
nums2 := [4]int{}
// 错误 nums1 [3]int 和nums2 [4]int是不同的类型
nums1 = nums2

切片(slice)

切片(slice)表示拥有相同类型元素的可变长度的序列。slice有三个属性:指针、长度、容量,内置函数 len()cap()可以获取 slice的长度和容量。

slice可以引用数组的任意位置,彼此之间的元素可以重复。slice无法被比较

nums := [...]int{1, 2, 3, 4, 5, 6}

// sub就是nums数组的slice sub == [2, 3, 4, 5, 6]
// [i:j]表示获取从i下标开始到j下标为止的元素
// [1:] 表示从第1个下标开始到最后一个下标的所有元素
// [:1] 表示从第0个下标开始到第1个下标的所有元素
sub := nums[1:]

// make()函数可以创建一个指定类型和长度的slice 这个slice的长度和容量都等于传入的值
arrays := make([]int, 10)

内置的 appen()函数可以将元素追加到一个 slice的后面。每一次调用 append函数都会检查目标 slice是否有足够容量来存储新元素,如果足够就会定义一个新的 slice(仍引用原始底层数组)添加新元素后再返回;如果容量不够,append函数会创建一个新的 slice并复制旧 slice的值,将新元素添加后再返回新的 slice

每次slice容量需要扩展时,通过扩展一倍的容量来减少内存分配的次数

// nums.len == 3 nums.cap == 3
nums := []int{1, 2, 3}
// 添加元素 slice的容量不够 扩展容量
// nums.len == 4 nums.cap == 6
nums = append(nums, 4)

map

map是一个拥有键值对元素的无序集合,map的类型是 map[K]V,键的类型 K必须可以比较并且在一个 map中,键的值是唯一的。键对应的值 V可以更新或移除,值类型 V没有任何限制。

// 声明map类型的三种方式

// 使用内置的make函数 创建一个指定类型的空map
cache := make(map[string]string)
// 使用空参数创建一个空map
nums := map[string]int{}
// map创建时添加初始化参数
ages := map[string]int {
    "a": 1,
    "b":2,
}
// map的访问也是通过下标的方式 如果key不存在则返回nil
age := ages["a"]
// 双参数获取形式,第一个返回值,第二个返回是否获取成功 bool值
age, ok := ages["a"]
if !ok {
    // 获取失败
}

使用内置的 delete()函数可以通过 key删除 map中的元素,如果 key不存在,此操作也是安全的

delete(ages, "a")

可以使用 for循环加 range关键字遍历一个 map,拿到其中所有的键和值。迭代顺序不是固定的

for key, value := range ages {
    fmt.Println(key, value)
}

需要按照固定的排序获取 map的值,需要先拿到所有的 key再进行排序,然后通过 key获取对于的值

names := make([]string, len(ages))
for key := range ages {
    names = append(names, key)
}
// 对key进行排序
sort.Strings(names)
// 拿到所有的值
for _, name := range names {
    fmt.Println(ages[name])
}

结构体

结构体是将零个或多个任意类型的变量组合在一起的聚合数据类型(类似 Java的类),每个变量都叫做结构体的成员。结构体通常都通过指针的方式使用

// 定义一个名称为Student的结构体 结构体和其中的成员都是可以导出的
type Student struct {
	Id       uint
	Name     string
	Age      uint
	Location string
}

// 空参结构体变量 所有的成员都是默认值
stu := Student{}
var stu Student
stu := &Student{}


// 带有默认参数的结构体 变量声明和默认值声明都有多种写法
stu := Student{
    Id: 1,
    Name: "xiao",
    Age: 18,
    Location: "china",
}
var stu Student = Student{2, "xin", 17, "china",}

// 获取和更改结构体变量中的成员属性
// 只能获取和更改提供了导出的成员属性
fmt.Println(stu.Name, stu.Age)
stu.Name = "xin"
stu.Age += 2
fmt.Println(stu.Name, stu.Age)

// 获取结构体变量成员的指针 通过指针更改值
p := &stu.Name
*p = "aa"

// 获取结构体变量的指针 通过指针获取和更改成员变量
stu1 := &stu
stu1.Name = "aaaa"
stu1.Age = 30

// stu.Name == aaaa  stu.Age == 30

结构体类型中不能定义一个相同类型的成员变量,但可以定义相同类型的指针类型,这样可以创建递归数据类型

type Student struct {
	Id       uint
	Name     string
	Age      uint
	Location string
    // 编译报错
	// Sub      Student
    // 可以使用指针类型
    Sub      *Student
}

如果结构体的成员很少并且是同一类型,那么可以使用更简便的方式声明

type Point struct {X, Y int}

p := &Point{1, 2}

出于性能的考虑,结构体在需要传递给函数或者从函数中返回时通常使用指针的方式。需要修改结构体中成员变量的函数也需要使用结构体指针

type Point struct{ X, Y int }

func main() {
	p := &Point{1, 2}
	add(p)
}

func add(p *Point) {
	fmt.Println(p.X + p.Y)
}

如果结构体中所有成员变量是可以比较的,那么这个结构体也是可以比较的

当一个结构体中有另一个结构体类型的成员变量,那么就称为结构体嵌套。go提供了结构体嵌套机制,可以将一个命名结构体作为另一个结构体的匿名成员使用

// 不使用匿名成员时 访问嵌套结构体需要一直使用.操作符
type Classes struct {
	Id   uint
	Name string
    City string
}

type Student struct {
	Id    uint
	Name  string
	Age   uint
	Class Classes
}

func main() {
	stu := &Student{}
	stu.Class.Name = "3班"
}

Go允许定义不带名称的结构体成员,只需要指定类型即可,这种结构体成员称为匿名成员

// 使用匿名成员进行改造 嵌套结构体存在同名变量时 还是需要加上结构体名称 如果不是同名变量 那么可以直接访问
type Classes struct {
	Id   uint
	Name string
	City string
}

type Student struct {
	Id   uint
	Name string
	Age  uint
	Classes
}

func main() {
	stu := &Student{}
	stu.City = "china"
	stu.Classes.Name = "aa"
}

JSON

Json是一种发送和接受信息的标准,Go通过标准库 encoding/jsonJson格式的编解码提供了支持

Go的数据结构转为 Json称之为 marshal,通过 json.Marshal实现

// 格式化数组/切片
nums := [...]int{1, 2, 3, 4, 5}
str, err := json.Marshal(nums)
if err != nil {
    fmt.Printf("err")
}
fmt.Printf("%s\n", str)

// 格式化map
cache := make(map[int]string)
cache[1] = "a"
cache[2] = "b"
// 忽略错误
str, _ := json.Marshal(cache)
fmt.Printf("%s\n", str)
// 格式化结构体
type Student struct {
	Id   uint
	Name string
}

func main() {
	stu := &Student{1, "xin"}
    // 格式化后Json中字段的名称就是结构体的成员变量
    // {"Id":1,"Name":"xin"}
	str, _ := json.Marshal(stu)
	fmt.Printf("%s\n", str)
}

如果需要在 Json中自定义结构体成员变量格式化后的字段名,可以使用成员标签定义实现

type Student struct {
    // `json:"id"` 自定义Json字段的名称为 id
	Id   uint `json:"id"`
	Name string `json:"name"`
}

func main() {
	stu := &Student{1, "xin"}
    // {"id":1,"name":"xin"}
	str, _ := json.Marshal(stu)
	fmt.Printf("%s\n", str)
}

marshal的逆操作将 Json字符串转化为 Go的数据结构称为 unmarshal,由 json.unmarshal实现

str := "{\"id\": 1, \"name\": \"xin\"}"
stu := &Student{}
if err := json.Unmarshal([]byte(str), stu); err != nil {
    fmt.Println("err")
}
// stu.Name == xin
fmt.Println(stu.Name)

此外还有 json.Decoderjson.Encoder的流式编解码器,用以从字节流中解析出 Json字符串和将 Go的数据结构解析成字节流

函数

函数声明

每个函数声明都包含一个名字、一个形参列表、一个可选的返回列表以及函数体。返回值也可以形参一样命名,每一个命名的返回值会声明一个局部变量并根据类型初始化为对应的零值。

如果一个函数的几个形参和返回值类型相同,那么类型只需要写一次

func add(x, y int) (sum int)  {
	sum = x + y
    // 声明了返回值变量 return语句会直接返回变量
	return 
}

func main(){
    // sum == 3
    sum := add(1, 2)
}
// 其他语言的常规写法
func add(x int, y int) int {
	return x + y
}

函数的类型称作函数签名,当两个函数拥有相同的形参列表和返回列表时,认为这两个函数的类型或签名是相同的

每一次调用函数都需要提供实参来对应函数的每一个形参。实参是按值传递的,函数接收到的是实参的副本,修改函数的形参变量并不会影响调用者提供的实参

多返回值

一个函数可以返回多个结果,常用的多返回值是一个期望得到的正确结果与一个错误值,或者表示函数调用是否成功的布尔值

// 读取txt文本文件的函数 返回文本文件的字符串和一个错误
func openTxt(name string) (res string, err error) {
	data, err := os.Open(name)
    // 文件打开失败返回空字符串和错误消息
	if err != nil {
		return "", err
	}
    // 逐行扫描文本 拼接后返回 此时err == nil
	scan := bufio.NewScanner(data)
	for scan.Scan() {
		res += scan.Text() + "\n"
	}
	return res, err
}

value, err := openTxt("demo.txt")
if err != nil {
    fmt.Printf("%v\n", err)
}
fmt.Println(value)

错误

函数并不总是成功返回的,当函数调用发生错误时返回一个附加结果作为错误值。习惯上将错误值作为最后一个结果返回。如果错误只有一种情况,结果通常设置为布尔类型。

函数返回的 error类型是内置的接口类型,可以通过调用它的 Error方法来获取错误消息。

可以使用 fmt.Errorf函数返回一个自定义的错误消息,它使用 fmt.Sprintf函数格式化一个错误消息并创建一个新的错误值

func openTxt(name string) (res string, err error) {
	data, err := os.Open(name)
	if err != nil {
		return "", fmt.Errorf("文件打开失败,错误消息:%v\n", err)
	}
	scan := bufio.NewScanner(data)
	for scan.Scan() {
		res += scan.Text() + "\n"
	}
	return res, err
}

错误的处理策略

  • 接收错误消息
  • 对于不固定和不可预测的错误,进行重试
  • 如果错误影响到程序运行,输出错误并停止程序
  • 记录错误消息并继续运行程序

函数变量

函数变量也有类型,它们可以赋给变量或者传递和从其他函数返回。函数变量可以像函数一样调用。

函数类型不可以比较并且零值是 nil,调用一个空函数变量会导致宕机。

func main() {
	f := add
    // sum == 3
 	sum := f(1, 2)
}

func add(x, y int) int {
	return x + y
}

做为回调函数被调用

type CallbackFunc func(string)

func callback(value string) {
	fmt.Println(value)
}

func main() {
	nums := []int{1, 2, 3, 4, 5, 6}
	selected(nums, 3, callback)
}

// 数组循环到指定的value时触发回调函数
func selected(values []int, value int, f CallbackFunc) {
	for _, num := range values {
		if num == value {
			fmt.Println(num)
			f("ok")
		}
	}
}

匿名函数

命名函数只能在包级别的作用域进行声明,能够使用函数字面量在任何表达式内指定函数变量。函数字面量就像函数声明,但是在 func关键字后面没有函数名称。它是一个表达式,值被称作匿名函数

以这种方式定义的函数能够获取到整个语法环境,因此,里层的函数可以使用外层函数的变量

func squares() func() int {
	var x int
	return func() int {
		x++
		return x * x
	}
}

func main() {
	f := squares()
	fmt.Println(f()) // 1
	fmt.Println(f()) // 4
}

函数 squarea返回了另一个匿名函数,调用 squarea会创建一个局部变量 x并返回一个匿名函数。每次调用都会递增 x的值然后返回 x的平方。

里层的匿名函数能够获取和更新外层函数的局部变量,函数变量类似于使用闭包的方法实现的变量,通常把函数变量称之为闭包

变长函数

变长函数被调用的时候可以提供可变的一个或多个参数个数

// nums参数个数可以是1个或者多个
func add(nums ...int) int {
	sum := 0
	for _, value := range nums {
		sum += value
	}
	return sum
}

func main() {
	fmt.Println(add(1, 2, 4, 10))
}

宕机和恢复

当宕机发生时,正常的程序执行会终止,所有的延迟函数会以倒序执行,然后程序异常退出并留下日志消息

内置的 panic()函数可以让 Go程序宕机,调用 panic函数时还可以传递一个错误消息

func main() {
	for i := 0; i < 10; i++ {
		if i == 4 {
			panic("宕机")
		}
		fmt.Println(i)
	}
}

内置的 recover()函数可以捕获宕机错误,当函数执行遇到宕机状态时,可以通过 recover函数从宕机状态恢复。

func main() {
    // 延迟调用捕获宕机 输出宕机的错误消息 函数不会报错 而是会正常退出
	defer func() {
		if r := recover(); r != nil {
			fmt.Println(r)
		}
	}()
	for i := 0; i < 10; i++ {
		if i == 4 {
			panic("宕机")
		}
		fmt.Println(i)
	}
}

方法

方法声明

方法的声明和函数的声明类似, 只是在函数名字前面多了一个参数,用于把这个方法绑定到对应的类型上。Go语言中接受者不使用特殊名(比如 thisself),而是使用自定义的接受者名称。

type Area struct {
	height uint
	wight  uint
}

// Area类型的方法
func (a Area) getSquare() uint {
	return a.wight * a.height
}

a := Area{2, 4}
// square == 8
square := a.getSquare()

为了避免调用方法时复制整个实参,可以使用指针来传递变量的地址。如果一个方法使用了指针接受者,那么该类型的所有方法都应该使用指针接收者

使用指针接受者对上面的方法进行改造

type Area struct {
	Height uint
	Wight  uint
}

func (a *Area) getSquare() uint {
	return a.Wight * a.Height
}

func (a *Area) edit(height, wight uint) {
	a.Height = height
	a.Wight = wight
}

func main() {
	a := Area{2, 7}
    // a.Height == 3 a.Wight == 4
	a.edit(3, 4)
}

上面的方法没有使用操作指针的关键字,Go语言的编译器会对变量和指针接收者进行隐式转换从而获取实际需要的值

方法变量与表达式

可以为方法赋予一个方法变量,它是一个函数,只需要提供实参不需要提供接收者就可以调用。

a1 := Area{2, 4}
// 方法变量
square := a1.getSquare
// result == 8
result := square()

方法变量相关的是方法表达式,和调用普通的函数不同。方法表达式把原来方法的接受者替换成函数的第一个形参。

a1 := Area{2, 4}
a2 := Area{3, 5}
// 由于方法使用了指针接收者 这里也必须传递指针
square := (*Area).getSquare
fmt.Println(square(&a1))
fmt.Println(square(&a2))

封装

如果变量或者方法不是通过对象访问到的,就叫做封装的变量和方法。

Go语言中封装的单元是包而不是类型,无论是在函数内的代码还是方法内的代码,结构体类型内的字段对于同一包中的所有代码都是可见的。

封装提供了三个优点:

  1. 使用方不能直接修改对象的变量,不需要更多的语句来检查变量的值
  2. 隐藏实现细节可以防止使用方依赖的属性发生改变
  3. 防止使用者随意更改对象内的变量

接口

接口类型是对其他类型行为的概括和抽象。Go语言的接口类型是隐式实现的,对于一个具体的类型,无须声明它实现了哪些接口,只要提供接口所必需的方法即可。

给上面的 Area类型添加一个 String()方法,那么它就实现了 fmt.Stringer接口

func (a *Area) String() string {
	return fmt.Sprintf("height:%v, wight:%v\n", a.Height, a.Wight)
}

接口类型

一个接口类型定义一套方法,如果一个类型要实现该接口,那么必须实现接口类型中定义的所有方法

// 声明一个接口
type Find interface {
	Search(keyword string) string
}

可以通过组合已有接口得到一个新的接口,这种语法称为嵌入式接口

// 组合Reader和Writer接口得到一个新接口
type ReadWrite interface {
	io.Reader
	io.Writer
}
// 声明方法和组合接口可以混合使用
type ReadWrite interface {
	Read(p []byte) (n int, err error)
	io.Writer
}

实现接口

一个类型实现了一个接口要求的所有方法,那么这个类型就实现了这个接口

一个特殊的类型是空接口类型,空接口类型对其实现类型没有任何要求,所有可以把任意值赋值给空接口类型

var i interface{}
i = true
i = 111
i = "a"
i = 1.1
i = []int {1, 2, 3}

无法直接使用空接口类型中的值,因为这个接口不包含任何方法。空接口和动态值为 nil的接口值是不一样的

error接口

error只是一个接口类型,包含一个返回错误消息的方法。构造 error最简单的方法就是调用 error.New,他会返回一个包含指定错误消息的 error实例。此外,还有一个更易用的 fmt.Errorf函数,额外提供了字符串格式化功能。

类型断言

类型断言是一个作用在接口值上的操作,会检查操作数的动态类型是否满足指定的断言类型,如果断言失败的话程序会崩溃。

var w io.Writer
w = os.Stdout
// 使用.()进行类型断言 
fmt.Println(w.(*bytes.Buffer))

在类型断言时,可以选择第二个参数用来判断断言是否成功,通常赋值给一个名为 okbool变量

var w io.Writer
w = os.Stdout
// 断言失败 b == nil ok == false 程序不会崩溃
b, ok := w.(*bytes.Buffer)

类型分支

接口的第二种风格充分利用了接口值能够容纳各种具体类型的能力,它把接口作为这些类型的联合来使用。类型断言用来运行时区分这些类型并且分别处理。在这种分格中,强调的是满足这个接口的具体类型,而不是这个接口的方法,也不注重信息隐藏。我们把这种风格的接口使用方式成为可识别联合。

类型分支的最简单形式与普通分支语句类似,两个的差别是操作数改为 x.(type),每个分支是一个或者多个类型

num := 123
switch num.(type) {
case nil:
case int, uint:
case string:
case bool:
default:
}

goroutine和通道

Go有两种并发编程的风格,分别是 goroutine和通道(channel

goroutine

Go中,每一个并发执行的活动成为 goroutine,有两个或多个 goroutine的并发程序中,不同的函数可以同时执行。

当一个程序启动时,只有一个 goroutine来调用 main函数,称它为 主goroutine,在普通的函数或方法调用前加上 go关键字即可创建一个新的 goroutinego语句本身的执行立即完成。

func wait() {
	time.Sleep(1 * time.Second)
	fmt.Println("sleep")
}

func main() {
	fmt.Println(time.Now().UnixMilli())
    // 等待wait函数执行完成
	wait()
	fmt.Println(time.Now().UnixMilli())
    // 不会等待执行完成 函数直接向下执行
	go wait()
	fmt.Println(time.Now().UnixMilli())
}

当main函数执行完毕或返回时,所有的 goroutine被暴力终结,然后程序退出

func handleConn(conn *net.Conn) {
	defer (*conn).Close()
	for {
		_, err := io.WriteString(*conn, time.Now().Format("2006-01-02 15:01:05\n"))
		if err != nil {
			return
		}
		time.Sleep(1 * time.Second)
	}
}

// 一个时钟服务器 监听tcp 8000端口 每秒向客户端推送一次时间
func main() {
	li, err := net.Listen("tcp", ":8000")
	if err != nil {
		log.Fatalln(err)
	}
	for {
		conn, err := li.Accept()
		if err != nil {
			continue
		}
        // 创建新的goroutine
		go handleConn(&conn)
	}
}
# 使用 nc (netcat)工具向端口发送请求
nc localhost 8000

通道(channel)

如果说 goroutineGo程序并发的执行体,通道就是它们之间的连接。通道是可以让不同 goroutine之间发送特定值的通信机制。每一个通道是一个具体类型的导管,叫做通道的元素类型。int类型的通道写为 chan int

// 使用内置的make函数创建通道
ch := make(chan int)

同种类型的通道可以比较,通道也可以和 nil进行比较

通道有三个主要操作:

  • 发送 send:通道和值放到 <-的左右两边
  • 接收 receive<-放到通道前面,接收到的值不被使用也是合法的
  • 关闭 close:使用内置的 close()函数关闭通道。设置一个标识位来指示值当前已经发送完毕,后续的发送操作将导致宕机。在一个已经关闭的通道上进行接收操作,将获取所有已经发送的值,直到通道为空。任何接收操作会立即完成,同时接收到一个通道元素类型对应的零值
ch := make(chan int)
go func() {
    // 接收到参数后关闭通道
    num := <-ch
    fmt.Println(num)
    close(ch)
}()
// 向通道中发送值
ch <- 1

使用简单的 make创建的通道是无缓冲通道make还可以接收第二个可选参数,表示通道容量的整数。

// 创建无缓冲通道 0也是创建无缓冲通道
ch := make(chan int)
ch := make(chan int, 0)
// 创建容量为4的缓冲通道
ch := make(chan int, 4)

无缓冲通道

无缓冲通道上的发送操作会阻塞,知道另一个 goroutine在对应的通道上执行接收操作,这时值发送完成,两个 goroutine都可以继续执行。

使用无缓冲通道进行的通信会导致发送和接收的 goroutine同步化,因此,无缓冲通道也称为同步通道

管道

通道可以用来连接 goroutine,当一个通道的输出是另一个通道的输入时,这种操作就称作管道

// 一个典型的管道操作 先向一个通道中发送数字 一个goroutine接收到值后向另一个通道发送经过处理后的值
// range循环语法用以在通道上迭代,可以接收在通道上所有发送的值,当通道关闭或者接收完最后一个值后结束循环
func main() {
	counts := make(chan int)
	squares := make(chan int)
	go func() {
		for i := 0; i <= 100; i++ {
			counts <- i
		}
		close(counts)
	}()
	go func() {
		for num := range counts {
			squares <- num * num
		}
		close(squares)
	}()
	for sum := range squares {
		fmt.Println(sum)
	}
}

单向通道类型

Go的类型系统提供了单向通道类型,仅仅导出发送和接收操作。类型 chan<- int是一个只能发送的通道,允许发送但不允许接收;类型 <-chan int是一个只能接收的类型通道,允许接收但是不能发送。

在赋值操作中将双向通道转换为单向通道是允许的,如果试图关闭一个只能接收的通道在编译时会报错

// 对上面的例子使用单向通道进行改造

func counter(in chan<- int) {
	for i := 0; i < 100; i++ {
		in <- i
	}
	close(in)
}

func squarer(in chan<- int, out <-chan int) {
	for num := range out {
		in <- num * num
	}
	close(in)
}

func pointer(out <-chan int) {
	for num := range out {
		fmt.Println(num)
	}
}

func main() {
	counts := make(chan int)
	squares := make(chan int)
	go counter(counts)
	go squarer(squares, counts)
	pointer(squares)
}

缓冲通道

缓冲通道类似于消息队列,在缓冲通道中有一个元素队列,队列的最大长度在创建的时候通过 make的容量参数来设置。

缓冲通道上的发送操作会在队列的尾部插入一个元素,接收操作会从队列的头部移除一个元素。如果通道满了。发送操作会阻塞直到缓冲通道可以接收值为止。

// 创建可以缓存3个元素的缓冲通道
ch := make(chan int, 3)
ch <- 1
ch <- 2
// 拿到缓冲通道的容量 3
fmt.Println(cap(ch))
// 拿到缓冲通道中的元素个数 2
fmt.Println(len(ch))

无缓冲和缓冲通道的选择、缓冲通道容量大小的选择,都会对程序产生影响。无缓冲通道提供强同步保障,缓冲通道提供操作解耦。如果知道要发送的值数量的上限,通常会创建一个容量是使用上限的缓冲通道

在内存无法提供缓冲容量的情况下,可能导致程序死锁

并行循环

需要遍历循环某一个数组对其中的每一项进行操作时,并行循环会需要非常久的时间。下面是一个非常典型的并行循环例子,有一个文件数组,需要将里面的每一项文件保存到本地

func saveFile(name string) {
	time.Sleep(1 * time.Second)
	fmt.Println(name)
}

func main() {
	fileNames := []string{"a.png", "b.png", "c.jpg"}
	start := time.Now().UnixMilli()
	for _, name := range fileNames {
		saveFile(name)
	}
	end := time.Now().UnixMilli()
    // handleTime == 3001
	fmt.Printf("handleTime:%d\n", end-start)
}

当保存文件项时,都会等待 saveFile函数执行完成才会再次循环。可以通过 go关键字创建 goroutine来异步保存文件。

sync.WaitGroup会创建一个 goroutine等待列表,每一次循环就往里面添加 1,当子 goroutine执行完成后调用 wg.Done方法来报告当前线程执行完成,最后在主 goroutine中调用 wg.Wait方法来等待所有子 goroutine执行完毕。

func main() {
	fileNames := []string{"a.png", "b.png", "c.jpg"}
	var wg sync.WaitGroup
	start := time.Now().UnixMilli()
	for _, name := range fileNames {
		wg.Add(1)
		go func(name string) {
			defer wg.Done()
			saveFile(name)
		}(name)
	}
	wg.Wait()
	end := time.Now().UnixMilli()
    // handleTime == 1001
	fmt.Printf("handleTime:%d\n", end-start)
}

select多路复用

select多路复用用于同时接收多个通道的值,select会一直等待,当有指定的通道发生通信时,它会执行此情况所对应的语句。

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go func() {
		time.Sleep(2 * time.Second)
		ch1 <- 1
	}()
	go func() {
		time.Sleep(3 * time.Second)
		ch2 <- 2
	}()

	select {
	case num1 := <-ch1:
		fmt.Println(num1)
	case num2 := <-ch2:
		fmt.Println(num2)
	case <-time.After(5 * time.Second):
		fmt.Println("响应超时")
	}
}

使用共享变量实现并发

竞态

当一个程序有两个或多个 goroutine在同时执行时,如果无法确认一个事件肯定优先于另一个事件,那么这两个事件就是并发的。

如果一个函数在并发调用时仍然能够正确工作,那么这个函数就是并发安全的,如果一个类型的所有可访问访问和操作都是并发安全的,则它可称为并发安全的类型。

对于绝大部分变量,如要回避并发访问,要么限制变量只存在与一个 goroutine内,要么维护一个更高层的互斥不变量

函数并发调用时不工作的原因有很多,包括死锁、活锁以及资源耗尽吗,竞态是指多个 goroutine按某些交错顺序执行时程序无法给出正确的结果

var balance int

func add(amount int) {
	balance += amount
}
func Balance() int {
	return balance
}

func main() {
	go func() {
		add(10)
        // 期望输出 10 实际输出可能为10也可能为30
        // 两个goroutine就发生了竞态
		fmt.Println(Balance())
	}()
	go func() {
		add(20)
	}()
	time.Sleep(time.Second)
}

避免数据竞态有多种方法

  1. 不要修改变量,如果变量初始化之后不会再修改,那么在多个 goroutine中就只会访问变量,不存在竞态

  2. 第二个方法是避免多个 goroutine访问同一个变量,再通过通道来向受限 goroutine发送查询请求或者更新变量。使用通道请求来代理一个受限变量的所有访问的 goroutine称为该变量的监控

    var inputs = make(chan int)
    var balances = make(chan int)
    
    func add(amount int) {
    	inputs <- amount
    }
    func Balance() int {
    	return <-balances
    }
    func filter() {
    	var balance int
    	for {
    		select {
    		case amount := <-inputs:
    			balance += amount
    		case balances <- balance:
    		}
    	}
    }
    func main() {
    	go filter()
    	go func() {
    		add(10)
    		fmt.Println(Balance())
    	}()
    	go func() {
    		add(20)
    	}()
    	time.Sleep(time.Second)
    }
    
  3. 第三种方法是允许多个 goroutine访问同一个变量,但是同一时间只有一个 goroutine可以访问。这种方法称为互斥机制

互斥锁 sync.Mutex

互斥锁模式应用非常广泛,sync包提供了一个单独的 Mutex类型来支持。它的 Lock方法用来获取锁,Unlock方法用于释放锁

var (
	counter int = 100
	wg      sync.WaitGroup
	mu      sync.Mutex
)

// 调用多个线程输出 会造成竞态 输出的结果不正确
func main() {
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go point()
	}
	wg.Wait()
	fmt.Println(counter)
}

func point() {
	defer wg.Done()
    // 使用互斥锁来保证同一时间只有一个goroutine可以访问和修改counter变量
	for counter > 0 {
		mu.Lock()
		fmt.Println(counter)
		counter--
		mu.Unlock()
	}
}

一个 goroutine在每次访问 counter变量前都必须先调用互斥锁的 Lock方法获取锁,如果其他 goroutine已经取得了锁,那么操作会一直阻塞到其他 goroutine释放锁之后。

LockUnlock之间的代码,可以自由地读取和修改共享变量,这一部分称为临界区域

读写互斥锁 sync.RWMutex

读写互斥锁允许只读操作可以并发执行,但写操作需要获取访问权限。sync包中 RWMutex类型提供的 RLockRUnlock可以分别用来获取和释放一个读锁

仅在绝大部分 goroutine都在获取读锁并且锁竞争比较激烈时,RWMutex才有优势,因为 RWMutex需要更复杂的处理,在竞争不激烈时它比普通的互斥锁慢

延迟初始化 sync.Once

sync.Once是一个针对一次性初始化问题的解决方案,Once包含一个和布尔变量和一个互斥量,布尔变量记录初始化是否已经完成,互斥量负责保护这个布尔变量和客户端的数据结构。Once的唯一方法 Do以初始化函数做为它的参数

var cache map[int]string
var once sync.Once

func load() {
	fmt.Println("cache init...")
	cache = map[int]string{
		0: "a",
		1: "b",
		2: "c",
	}
}

func main() {
	for i := 0; i < 3; i++ {
		once.Do(load)
		fmt.Println(cache[i])
	}
}

竞态检测器

Go语言运行时和工具链包含了这个易用的动态分析工具。将 -race命令行参数添加到 go build、go run、go test命令里面就可以使用该功能,它会让编译器测试构建一个修改后的版本,这个版本有额外的手法用于高效记录在执行时对共享变量的所有访问、以及读写这些变量的 goroutine标识。

goroutine和线程

可增长的栈

每个 OS线程都一个一个固定大小的栈内存(通常为 2MB),栈内存区域用于保存在其他函数调用执行期间那些正在执行或临时暂停的函数中的局部变量。固定大小的栈始终不够大,改变这个固定大小可以提高空间效率并创建更多的线程。

一个 goroutine在生命开始周期只有一个 2KB大小的栈,goroutine栈和 OS的栈类似,但是并不是固定大小,它可以按需增大和缩小。goroutine的栈大小限制可以达到 1GB

goroutine调度

OS线程由 OS内核来调度。每隔几毫秒,一个硬件时钟中断发送 CPUCPU 调用一个叫调度器的内核函数 。 这个 函数暂停当前正在运行的线程, 把它的寄存器信息保存到内存,查看线程列表并决定接下来运行哪一个线程,再从内存恢复线程的注册表信息,最后继续执行选中的线程。因为 OS 线程由内核来调度,所以控制权限从一个线程到另外一个线程需要一个完整的上下文切换(context switch): 即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器 的数据结构 。考虑这个操作涉及的内存局域性以及涉及的内存访问数量 ,还有访问内存所需的 CPU 周期数量的增加,这个操作其实是很慢的。

Go 运行时包含一个自己的调度器,这个调度器使用一个称为 m:n调度的技术(因为它可以复用/调度 m 个 goroutine 到 n 个 OS 线程))。Go调度器与内核调度器的工作类似,但 Go调度器只需关心单个 Go程序的 goroutine调度问题

与操作系统的线程调度器不同的是,Go调度器不是由硬件时钟来定期触发的,而是由特定的 Go语言结构来触发的 。比如当一个 goroutine 调用 time.Sleep或被通道阻塞或对互斥量操作时,调度器就会将这个 goroutine设为休眠模式,并运行其他 goroutine直到前一个可重新唤醒为止 。因为它不需要切换到内核语境,所以调用一个 goroutine,比调度 一个线程成本低很多。

goroutine没有标识

在大多数支持多线程的操作系统和编程语言中,当前线程都有一个独特的标识。这个特性可以轻松构建一个线程局部存储(例如 JavaThreadLocal

goroutine没有可供程序员访问的标识。这是由设计决定的,因为线程局部存储有一种被滥用的倾向。

包和Go工具

包和导入

在每一个 Go源文件的开头都需要进行包声明。主要的目的是该报被其他包引入的时候作为其默认的标识符(称为包名)

一个 Go源文件可以在 package声明的后面和第一个非导入声明语句前面紧接着包含零个或多个 import声明。每一个导入可以单独指定一条导入路径,也可以通过圆括号来一次导入多个包。

package main

import (
	"fmt"
	"sync"
)

导入的包可以通过空行进行分组,这类分组通常表示不同领域和方面的包。导入顺序不重要,当按照惯例每一组都按照字母进行排序

如果需要包两个名字一样的包导入到第三个包中,导入声明就必须为其中的一个指定一个别名来比避免冲突。也被称作重命名导入

import (
	mrand "math/rand"
	"crypto/rand"
)

如果导入的包没有在文件中引用,就会产生一个编译错误。但是,有时候我们必须导入一个包而不会显式使用它。使用空白标识符 _来重命名导入,用以实现一个编译时的机制,使用空白引用来导入额外的包应以启用主程序中可选的特性。

import (
	_ "image/png"
)

创建一个包时,尽量使用简短的名字,同时尽可能保持可读性和无歧义

Go工具

go工具将不同种类的工具集合并为一个命名集。它是一个包管理器,可以查询包的作者、计算依赖关系、下载远程管理仓库中的包。也是一个构建系统,可以计算文件依赖、调用编译器、汇编器和链接器。同时它还是一个测试驱动程序

工作空间

GOPATH表示工作空间的环境变量,当需要切换不用的工作空间时,切换 GOPATH的值即可

# 查看当前工作空间
go env | grep GOPATH

# 设置新的工作空间
export GOPATH=/home/xxx/xxx

GOPATH下有三个子目录

  • srcsrc目录包含源文件,每一个包放在一个目录中
  • pkgpkg子目录是构建工具存储编译后的包的位置
  • binbin子目录放置可执行程序

第二个环境变量是 GOROOT,执行 Go发行版的根目录,其中提供所有标准库的包

包的下载

使用 go get命令来下载单一的包,也可以使用 ...符号来下载子树或仓库,当完成下载后,它会构建包然后安装库和相应的命令

如果指定了 -u参数,go get将会下载包的最新版本

包的文档化

Go的文档注释总是完整的语句,使用声明的包名作为开头的第一节注释通常是总结。函数参数和其他的标识符无须括起来或者特别标注

包声明的前面的文档注释被认为是整个包的文档注释,比较长的文档注释可以使用一个单独的注释文件,文件名通常叫 doc.go

go doc工具输出在命令行上指定内容的声明和整个文档注释,该工具不需要完成的导入路径或者正确的标识符

# 包注释
go doc time

# 包成员
go doc time.Second

# 方法
go doc time.Sleep

测试

go test工具

go test子命令是 Go语言包的测试驱动程序。在一个包目录中,以 _test.go结尾的文件不是 go build命令编译的目标,而是 go test编译的目标。

*_test.go文件中,三种函数需要特殊对待,即功能呢测试函数、基准测试函数和示例运行函数。

  • 功能测试函数以 Test前缀命名,用来检测一些程序逻辑的正确性,go test运行测试函数并且报告结果是 PASS还是 FAIL
  • 基准测试函数以 Benchmark前缀开头,用来测试某些操作的性能,go test汇报操作的平均执行时间
  • 示例函数的名称以 Example开头,用来提供及其检查过的文档

Test函数

每一个测试文件必须导入 testing包,功能测试函数必须以 Test开头,可选的后缀名称必须以大写字母开头

import (
	"strings"
	"testing"
)
// 判断一个字符串是否是回文字符串
func isPalindrome(text string) bool {
	text = strings.ReplaceAll(text, " ", "")
	text = strings.ToUpper(text)
	for i := 0; i < len(text)/2; i++ {
		if text[i] != text[len(text)-1-i] {
			return false
		}
	}
	return true
}

// 测试函数
// 参数 t 提供了汇报失败和日志记录的功能
func TestIsPalindrome(t *testing.T) {
	texts := []string{"hello olleh", "yes sye", "noon", "xiaooaix"}
	for _, text := range texts {
		if result := isPalindrome(text); !result {
			t.Errorf("False Text:%v\n", text)
		}
	}
}

go test命令在不指定包参数的情况下,以当前所在目录所在的包为参数。-v参数可以输出包中每个测试用例的名称和执行的时间

go test -v

可选的 -run参数是一个正则表达式,可以让 go test只运行那些测试函数名称匹配的函数

编写有效测试

Go希望测试的编写者自己来做大部分的工作,通过定义函数来避免重复,测试的过程不是死记硬背地填表格。一个好的测试不会在发生错误时崩溃,而是输出该问题一个简洁、清晰的现象描述,以及其他与上下文相关的信息。理想情况,维护者不需要再通过阅读源代码来探究测试失败的原因。

Benchmark函数

基准测试就是在一定的工作负载下检测程序性能的一种方法,基础测试函数看起来像一个测试函数,但是额外增加了一些与性能检测相关的方法。还提供了一个整形成员 N,用来指定被检测操作的执行次数

// IsPalindrome函数的基准测试函数
func BenchmarkIsPalindrome(b *testing.B) {
	for i := 0; i < b.N; i++ {
		isPalindrome("hello o l l e h")
	}
}

使用 go test -bench=命令来指定运行基准测试,=号后面匹配基准测试函数的名称,可以使用 .来运行所有基准测试函数。

go test -bench=.

# 运行结果
goos: linux
goarch: amd64
pkg: demo
cpu: AMD Ryzen 7 6800H with Radeon Graphics       
BenchmarkIsPalindrome-16        11054334               191.9 ns/op
PASS
ok      demo    2.237s

基准测试名称的数字后缀表示 GOMAXPROCS值,报告说明每次 IsPalindrome函数调用耗时 191ns,这是 11054334次运行的平均值

基准测试运行器开始的时候并不知道操作的耗时长短,所有使用较小的 N值做检测,然后为了检测稳定的运行时间,再推断出足够大的 N值。

Example函数

示例函数有三个目的,首要目的是作为文档,示例函数是 Go代码,必须通过编译时检查,基于 Web的文档服务器 godoc可以将示例函数和它所演示的函数或包关联。

第二个目的是它们可以是可以通过 go test运行的可执行测试。如果一个示例函数最后包含一个类似 // out:的注释,测试驱动程序将执行这个函数并且检查输出到中的的内容是否匹配这个注释中的文本。

第三个目的是提供手动实验代码,在 godoc文档服务器上使用 Go Playground来让用户在浏览器上编辑和运行示例函数

反射

反射和低级编程暂时用不到,后面学了再续上

低级编程

关联文章

0 条评论