go version go1.11 windows/amd64

 

本文为阅读Go语言中文官网的规则说明书(https://golang.google.cn/ref/spec)而做的笔记,介绍Go语言的 内建函数(Built-in functions)。

 

规格说明书中的目录如下:

Built-in functions
  -Close
  -Length and capacity
  -Allocation
  -Making slices, maps and channels
  -Appending to and copying slices
  -Deletion of map elements
  -Manipulating complex numbers
  -Handling panics
  -Bootstrapping

 

在规格说明书中,介绍了15个内建函数,如下所示:

close, len, cap, new, make, append, copy, delete,
complex, real, imag, panic, recover,
print, println

 

下面简要介绍各个函数:

close

关闭信道(channel)。

在没有更多数据 要 发送 的时候关闭信道。

close(channel)

也就是说,channel必须是 只发 或 双向类型的才可以,否则错误。

发送 或 关闭 一个已经关闭的 信道 会出现 运行时错误(run-time panic)。

关闭 nil 信道 也会导致 运行时错误。

在调用close函数,并且之前发送的数据都已被接收到 之后,接收操作


// 单个值接收操作
v1 := <-ch
v2 = <-ch

// 多(两)个值接收操作
x, ok = <-ch
x, ok := <-ch
var x, ok = <-ch
var x, ok T = <-ch

 

len,cap

这两个函数接收各种类型的参数,返回一个int型的结果。

len函数返回参数的 长度、元素个数,cap返回参数的 容量。

官文介绍:

Call      Argument type    Result

len(s)    string type      string length in bytes
          [n]T, *[n]T      array length (== n)
          []T              slice length
          map[K]T          map length (number of defined keys)
          chan T           number of elements queued in channel buffer

cap(s)    [n]T, *[n]T      array length (== n)
          []T              slice capacity
          chan T           channel buffer capacity

从官文介绍可以看出,len函数 的参数可以为 字符串、数组类型、数组指针、分片、映射类型、信道——在信道缓存中排队的元素数量,

                                    cap函数 的参数可以为 数组、数组指针、分片、信道——缓存容量大小。

分片类型 的容量是指 分配给它的 底层数组 的元素的数量。在任何时候,下面的公式是成立的:


0 <= len(s) <= cap(s)


值为 nil 的 分片、映射、信道 的长度为0,值为 nil 的分片的容量为 0。

 

如果 s 是 字符串常量,那么,表达式 len(s) 也是常量;

如果 s 是 数组 或者 指向数组的指针,那么,表达式 len(s) 和 cap(s) 也是常量,并且 s 的元素类型不是 接收信道 或 函数类型,在这种情况下,s 是无法被估值的(evaluated);

除了上面的情况外,调用 len 和 cap 得到的都不是常量,而且s是可以估值的(翻译的有些不准确,这个evaluated到底是什么意思?)。

 

官文示例:



const (
	c1 = imag(2i)                    // imag(2i) = 2.0 is a constant
	c2 = len([10]float64{2})         // [10]float64{2} contains no function calls
	c3 = len([10]float64{c1})        // [10]float64{c1} contains no function calls
	c4 = len([10]float64{imag(2i)})  // imag(2i) is a constant and no function call is issued
	c5 = len([10]float64{imag(z)})   // invalid: imag(z) is a (non-constant) function call
)
var z complex128



 

new

new函数 在学习接口类型时遇到了,使用new,可以新建一个类型的实例。

new函数 属于 Allocation(分配) 小节,分配 什么呢?内存空间了。

官文翻译:

在 运行时,给 变量 分配一个 存储空间,这个空间大小由 变量类型决定,并且 返回 一个 变量类型的指针 指向分配的空间。

在分配完存储空间后,再进行初始化——分配完毕后是0值。

 

疑问,不能在分配时进行初始化吗?需要试验!



type S1 struct {
	int
	float32
}

var sv2 = new(S1{23, 32.0}) // 错误:S1 literal is not a type
fmt.Println(sv2)
fmt.Println(sv2.int, sv2.float32)



看来不能在调用new函数时赋值!或者俺的方法不对!

更改代码后测试:



var sv2 = new(S1)
sv2.int = 23
sv2.float32 = 32.0
fmt.Println(sv2)
fmt.Println(sv2.int, sv2.float32)

// 测试结果
// 注意第一行的 & 符号!
// 返回的 sv2 是一个 指针
&{23 32}
23 32


 

官文示例:


type S struct { a int; b float64 }
new(S)

 

make

make函数 属于 Making slices, maps and channels 一节的唯一函数,用于常见 分片、映射和信道。

make函数 的第一个参数为 类型T,可以选择跟着一个表达式的指明类型的列表(原文:optionally followed by a type-specific list of expressions,翻译存在问题)。

返回 类型T,而不是 *T,,空间初始化可以参考initial values

官文中介绍的用法:



Call             Type T     Result

make(T, n)       slice      slice of type T with length n and capacity n // 第二个参数n表示长度,没有第三个参数时,n也表示容量
make(T, n, m)    slice      slice of type T with length n and capacity m // 第二个参数n表示长度,第三个参数m表示容量

make(T)          map        map of type T // 映射,但没有分配初始空间
make(T, n)       map        map of type T with initial space for approximately n elements // 分配了初始空间的映射,可以容纳n个元素,但可以自动扩展(需确认)

make(T)          channel    unbuffered channel of type T // 无缓冲信道
make(T, n)       channel    buffered channel of type T, buffer size n // 缓冲区大小为 n(字节吗?)的信道,关于信道的资料,自己还需dig



每一个n、m都必须是 整型(integer type,各种整型吗?),或者一个 无类型的常量。一个常量大小参数不能是非负数,并且可以被类型int的值表示,,如果是无类型的常量,会被分配类型int。

n不能大于m。

如果n在运行时 为 负数 或 大于m,会产生一个运行时错误。

 

官文示例——合法、非法使用的都有:



s := make([]int, 10, 100)       // slice with len(s) == 10, cap(s) == 100
s := make([]int, 1e3)           // slice with len(s) == cap(s) == 1000
s := make([]int, 1<<63)         // illegal: len(s) is not representable by a value of type int // 1<<63超过类型int的范围了
s := make([]int, 10, 0)         // illegal: len(s) > cap(s)
c := make(chan int, 10)         // channel with a buffer size of 10
m := make(map[string]int, 100)  // map with initial space for approximately 100 elements



 

调用指定了类型和参数n的make函数创建映射时,会创建一个可以包含n个元素的初始化空间,但是,空间大小是和编译器的实现相关的(implementation-dependent)——这个大小是无法统一。

 

append,copy

这两个函数用来操作分片。从函数名可知,append用来给分片添加元素,copy用来从分片拷贝元素,拷贝到哪里呢?拷贝到分片中。

对于这两个函数,其返回结果是和 参数指向的内存是否覆盖(whether the memory referenced by the arguments overlaps) 没有关系的。

 

可变参函数append添加0到多个到分片,并返回 结果分片。

参数x中的值得类型T,必须是分片S的元素类型,但有一个特例,第一个参数为 []byte类型,第二个参数为string类型——但其结尾添加三个英文句号(...),这种情况表示将添加字符串的字节数组到第一个参数。


append(s S, x ...T) S  // T is the element type of S


如果分片s的容量不足于包含新添加的值,appen将分配新的、足够大的 底层数组 来满足 存在的分片和新增值 的空间,,否则,appen函数将使用 底层数组。

疑问,前面讲到,append会返回新的分片,那么,这个分片和旧的分片是什么关系呢?是在旧的分片上操作的吗?需要dig!之前自己操作时,是需要把append的返回值 赋值给 旧分片的变量的。

官文示例:



s0 := []int{0, 0}
s1 := append(s0, 2)                // append a single element     s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7)          // append multiple elements    s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...)            // append a slice              s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...)   // append overlapping slice    s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}

// 下面这个很有意思!用到了 空接口,分片可以包含 任何类型 的数据了
var t []interface{}
t = append(t, 42, 3.1415, "foo")   //                             t == []interface{}{42, 3.1415, "foo"}

var b []byte
b = append(b, "bar"...)            // append string contents      b == []byte{'b', 'a', 'r' }



 

copy函数从 源分片拷贝若干元素到目的分片,并返回拷贝的元素的数量。源分片和目的分片的元素类型T必须相同,并可以被赋值给[]T。

拷贝的元素的数量是 len(src)、len(dst) 中的最小值。

和append函数类似,copy函数也存在特例,目的分片为 []byte类型,源分片为字符串类型(string),这表示将字符串的字节数组拷贝到字符分片中。注意,特例不需要三个英文句号(...),这和append函数不同。

copy函数签名:

copy(dst, src []T) int
copy(dst []byte, src string) int


copy函数官文示例:

var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:])            // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:])            // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!")  // n3 == 5, b == []byte("Hello")

 

delete

删除一个映射 m 中的键值为 k 的元素。k 的类型 必须是 可以赋值(assignable,这个不是很清楚什么意思,类型相同 或 结果的类型相同) 给 映射 m 的键类型的。


delete(m, k)  // remove element m[k] from map m

如果映射 m 的值为 nil,或者,m[k] 不存在,那么,调用函数 delete 将什么也不做(no-op)。

 

complex,real,imag

这三个函数属于 操作复数(Manipulating complex numbers) 这一节。

大概意思:

complex函数 根据参数的 浮点类型的 实部、虚部 建立复数,real函数 获取一个复数的 实部,imag函数 获取一个复数的 虚部。

官文的三个函数签名:



complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT



 

complex的两个参数必须是相同的浮点类型——float32、float64,float32生成复数类型为complex64,float64生产的复数类型为complex128。

如果一个参数是 没有类型的常量(untyped constant),它的类型就被转换为另一个参数的类型;

如果两个参数都是 没有类型的常量,它们必须 不是复数、或者它们的虚部为0,并且返回的是一个 没有类型的复数(特别)。

 

对于real、imag函数,参数必须是复数类型,返回值的类型由参数的类型决定,对应关系:complex64-->float32,complex128-->float64。

如果参数时一个 没有类型的常量,那么,参数必须是数,并且返回值是 没有类型的浮点常量(特别)。

 

real、imag函数 一起 形成了 complex函数 的反函数,对于一个复数类型Z的值z,下面的公式是成立的:


z == Z(complex(real(z), imag(z)))


 

如果这些函数的操作数都是 常量,那么,返回值也是常量。

 

官文示例:



var a = complex(2, -2)             // complex128
const b = complex(1.0, -1.4)       // untyped complex constant 1 - 1.4i
x := float32(math.Cos(math.Pi/2))  // float32
var c64 = complex(5, -x)           // complex64
var s uint = complex(1, 0)         // untyped complex constant 1 + 0i can be converted to uint
_ = complex(1, 2<<s)               // illegal: 2 assumes floating-point type, cannot shift // 整数才可以做 位运算
var rl = real(c64)                 // float32
var im = imag(a)                   // float64
const c = imag(b)                  // untyped constant -1.4
_ = imag(3 << s)                   // illegal: 3 assumes complex type, cannot shift // 整数才可以做 位运算



 

panic,recover

这两个函数分别用于 报告 和 处理 运行时错误 或 程序自定义的错误条件,签名如下:


func panic(interface{})
func recover() interface{}


当然,panic函数 就是 报告一个错误,recover函数 就是负责处理的函数。

 

特别提醒,要读懂这一节,需要理解 Go语言中 的 Defer statements(延迟语句?)

 

下面是官文翻译:

在执行 函数F 的时候,显式调用panic函数或者发生运行时错误(run-time panic)都会 终断函数F的执行,但是,任何被 函数F 延迟的函数 会立即被执行。

也就是说,函数F中发生错误了,如果没有 延迟函数,那么,函数F 被终断,如果有延迟函数,则执行完延迟函数后再终断。

前面讲到 函数F 的延迟函数被调用,所有的延迟函数执行完毕后,,接下来,函数F 的调用者 的延迟函数 开始执行,,直到goroutine的顶级函数的延迟函数被执行。

看到了吧,发生了错误,就一直是 延迟函数 在执行,直到goroutine的顶级函数的延迟函数被执行。那么,什么事goroutine呢?goroutine的顶级函数的延迟函数被执行 之后怎么样呢?销毁goroutine?

在这个时刻,程序被终止,错误条件被报告,同时被报告的还有函数panic的参数的值。这个终止序列被称为 panicking(翻译为什么好呢?惊险追踪?错误追踪?)。

官文示例——调用panic函数:


panic(42)
panic("unreachable")
panic(Error("cannot parse"))



上面讲了panic函数,用于 显示制造错误,在错误发生后,延迟函数开始执行,直到goroutine的顶级函数的延迟函数被执行完毕。

注意,延迟函数 在没有发生错误时也会执行,时机为 函数返回前(有返回return语句的) 和 函数结束前。

 

从上面也看到了,如果不阻止panic的扩散,程序(goroutine?这个自己还不完全清楚)会被终止!那么,怎么阻止呢?这就需要 recover函数 了!

 

recover函数 使用一段程序 去管理一个panicking goroutine的行为。

设想一下,函数G 延迟了 函数D 的执行,函数D中调用了 recover函数。

在同一个goroutine中,函数G发生了panic(错误),此时,被函数G延迟的函数开始执行——其中包括函数D,当执行到函数D时,函数D 中调用recover函数 的返回值 是传递到 调用panic函数中的值。

如果 函数D 正常返回,也没有产生新的panic,panicking这个过程就结束了

在这种情况下,调用函数G到调用函数panic(发生错误)期间的函数状态就被忽略了,并且继续程序的正常执行。

前面到了函数D被执行了, 返回了,panicking结束了,此时,在函数D之前被函数G 延迟的 函数开始执行——如果有,函数G的执行被终止,返回它的调用者。

以上,便是执行recover函数的“内幕”!调用recover且没有产生新的panic就表示错误被处理了。程序还是在函数G中执行,但执行的是函数G的延迟函数,执行完延迟函数了,就返回到它的调用者。

 

前面提到 recover函数 有返回值,值为panic函数的参数。那么,recover函数调用后什么时候其返回值为 nil 呢?下面几种情况之一满足即返回 nil:

-panic函数的参数为 nil (空吗?)

-goroutine没有发生panicking——就是没错的时候调用recover恢复;

-recover函数不是被延迟函数直接调用的——这是什么情况?还需dig!

 

官文示例:protect函数 调用了 参数中的 函数g,并延迟了一个函数 用于处理 调用函数g 引发的panic(run-time panics)。



func protect(g func()) {
	defer func() { // 延迟函数定义,关键字 defer
		log.Println("done")  // Println executes normally even if there is a panic
		if x := recover(); x != nil { // 调用recover:如果发生错误,返回值 x 就不是 nil,此时,处理错误,只是打印一条信息
			log.Printf("run time panic: %v", x)
		}
	}()
	log.Println("start")
	g()
}



 

几乎理解了,还需要熟悉 goroutine,以及阅读和练习更多Go代码才行啊!

 

print,println

目前的Go语言实现提供了一些在引导时有用的内建函数,文档为了完整性会介绍这些函数,但是,不保证它们会存在于Go语言中,而且它们不会返回结果。

比如,本节的print、println两个函数,都用来输出 参数。

官文函数说明:

Function   Behavior

print      prints all arguments; formatting of arguments is implementation-specific
println    like print but prints spaces between arguments and a newline at the end


实现(所谓的实现,是指,各个软件开发组织 根据 这份规格说明书 来开发Go语言编译器的实现吧)要求:

print、pringln不需要接受所有的参数类型,但是,打印 布尔、数值、字符串类型 是必须支持的。

 

后记

又是小半个下午加晚上的一两个小时,这篇博文可是花了5个小时左右啊!

缺少一些示例,自己的实践,不够完美,但是呢,大部分知识点是讲清楚了,不清楚的也是有标记的。