文章目录
声明和赋值
声明
声明给一个程序实体命名,并且设定其部分或全部属性。
Go 中有四个主要的声明:变量(var),常量(const),类型(type)和函数(func)。
Go 中只有一种方式控制命名的可见性:定义的时候,首字母大写的标识符是可以从包中导出的,而首字母没有大写的则不可导出。
变量声明
变量声明有下面几种方式:
a := 10
var b int
var c = 10
var d int = 10
e := new(map[string]string) // &map[]
f := make(map[string]string, 2) // map[]
- 第一种更加简洁,但是通常在一个函数内部使用,而且不适合包级别的变量。
- 第二种根据变量的类型默认初始化为类型的零值。
- 第三种很少使用。
- 第四种是显式的变量类型,在类型一致的情况下是冗余的,在类型不一致的情况下是必须的。
- 第五种使用内置的 new 函数,参数为类型,返回值为该类型下零值的指针。
- 第六种使用内置的 make 函数,参数为类型和长度,返回该类型下的引用。
通常使用前两种形式,使用显式的初始化来说明初始化变量的重要性,使用隐式的初始化来表明初始化变量不重要。
在 Go 中,只声明变量而不使用是会报错的。
“:=”表示声明,而“=”表示赋值。
new 的作用是 初始化 一个指向类型的指针 (*T), make 的作用是为 slice, map 或者 channel 初始化,并且返回引用 T。
常量声明
常量是一种表达式,其可以保证在编译阶段就计算出表达式的值。
所有常量本质上都属于基本类型:布尔型、字符串或数字。
const pi= 3.14
const(
e= 2.71
pi=3.14
)
对于常量操作数,所有数学运算,逻辑运算和比较运算的结果依然是常量;常量的类型转换结果和某些内置函数的返回值,例如:len,cap,real,imag,complex 和 unsafe.Sizeof,同样是常量。
若同时声明一组常量,除第一项之外,其他在等号右侧的表达式都可以省略,表示复用前面一项的表达式及其类型。
const(
a=1
b
c=2
d
)
常量生成器 iota,通常称为枚举类型。
// A Weekday specifies a day of the week (Sunday = 0, ...).
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
type Flags uint
const (
FlagUp Flags = 1 << iota // interface is up
FlagBroadcast // interface supports broadcast access capability
FlagLoopback // interface is a loopback interface
FlagPointToPoint // interface belongs to a point-to-point link
FlagMulticast // interface supports multicast access capability
)
许多常量并不从属于某些具体的类型,编译器将这些常量表示成某些值,这些值比基本数字类型的数字精度更高,且算术精度高于原生的机器精度。可以认为精度至少为 256 位。从属类型待定的常量共有 6 种:无类型布尔,无类型整数,无类型文字符号,无类型浮点数,无类型复数,无类型字符串。
只有常量才可以是无类型的,借助推迟确定从属类型,无类型常量不仅可以暂时维持更高的精度,还可以写进更多的表达式而无需转换类型。
字面量的类型由语法决定。0,0.0,0i 和‘\u0000’ 都表示相同的常量值,但类型分别为:无类型整数,无类型浮点数,无类型复数和无类型文字符号。
Go 语言中只有大小不明确的 int 类型,没有大小不明确的 float 类型,因为 float 类型的大小不明确就很难写出正确的数值算法。
类型声明
类型声明定义一个新的命名类型,它和某个已有类型使用同样的底层类型。
type name underlying-type
函数声明
函数声明包含一个名字、形参列表、一个可选的返回列表以及函数体。
func name(parameter-list) (result-list) {
}
赋值
赋值语句用来更新变量所指的值。
var a, b int
a, b = 1, 2
v, ok = m[key] // map 查询
v, ok = x.(T) // 类型断言
v, ok = <-ch // 通道接收
Go 允许多重赋值,即几个变量一次性被赋值。
map 查询,类型断言和通道接收在赋值语句中会产生一个额外的布尔型结果。
不允许隐式的类型转换,赋值时类型必须精确匹配;nil 可以被赋值给任何接口变量或引用类型。
流程控制
package main
import "fmt"
func main() {
// 1 if-else 条件判断
var flag bool = true
if flag {
fmt.Println("ok")
} else {
fmt.Println("fail")
}
if i := 1; flag {
fmt.Println("ok with: ", i)
} else {
fmt.Println("fail with: ", i)
}
// 2 for 循环
var total int = 0
for i := 1; i <= 100; i++ {
total += i
}
fmt.Println(total)
// 3 break
for i := 1; i < 10; i++ {
if i == 5 {
break
}
fmt.Print(i)
}
fmt.Println()
// 4 continue
for i := 1; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Print(i)
}
fmt.Println()
// 5 range
list := [] string{"I", "T", "A", "E", "M"}
for k, v := range list {
fmt.Println(k, v)
}
// 6 switch
choice := 2
switch choice {
case 1:
fmt.Println(1)
case 2:
fmt.Println(2)
case 3:
fmt.Println(3)
default:
fmt.Println("others")
}
// 7 输入
var input int
fmt.Scanln(&input)
fmt.Println(input)
// 8 延时加载:类似栈
for i := 0; i < 5; i++ {
defer fmt.Print(i) // 4 3 2 1 0
}
}
数据类型
基础类型
数字
Go 的数值类型包括了几种不同大小的整数、浮点数和复数。
整数
- 有符号整数:int8、int16、int32、int64
- 无符号整数:uint8、uint16、uint32、uint64
- int 和 uint(不能确定为 32 位或 64 位)
无符号整数往往只用于位运算和特定算术运算符,如实现位集时,解析二进制格式的文件,或散列和加密。一般而言,无符号整数极少用于表示非负值。
浮点数
- float32:有效数字约为 6 位
- float64:有效数字约为 15 位
应优先使用 float64 因为除非格外小心,否则 float32 的运算会迅速累积误差,并且float32 能精确表示的正整数范围有限。
- +Inf:正无穷大
- -Inf:负无穷大
- NaN:数学上无意义的运算结果,e.g. 0/0 或 Sqrt(-1)
- 与 NaN 的比较总是不成立的,但是!=和==的结果总是相反的
t := math.NaN()
fmt.Println(t == t, t > t, t < t, t != t) // false false false true
复数
- complex64,由 float32 构成
- complex128,由 float64 构成
var x complex128=complex(1,2)// 无类型复数转 complex128
var y complex128= complex(3, 4)
fmt.Println(x*y)
fmt.Println(real(x*y))
fmt.Println(imag(x*y))
// (-5+10i)
// -5
// 10
布尔型
- true
- false
布尔值无法隐式转换成数值(如 0或 1),反之也不行。
字符串
字符串是不可变的字节序列,它可以包含任意数据。
- len 函数:返回字符串的字节数
- 下标访问操作s[i]:访问第 i 个字符(0≤i<len(s))
因为字符串不可改变,所以字符串内部的数据不允许修改。
不可变意味着两个字符串能安全地共用同一段底层内存,使得复制任何长度字符串的开销都低廉。同时字符串和其子串可以安全地共用数据,因此子串生成操作开销低廉。两种操作都没有分配新内存。
s:= "hello, world"
hello:=s[:5]
world:=s[7:]
fmt.Println(hello)
fmt.Println(world)
字符串字面量
Go 的源文件总是按 UTF-8 编码,所以可以将 Unicode 码点写入字符串字面量。
原生的字符串字面量的书写形式是`…`,使用反引号。唯一的特殊处理是删除回车符,保留换行符,这样同一字符串在所有平台上的值都相同。
rune
UTF-8 是以字节为单位对 Unicode 码点作变长编码,UTF-8 是现行的一种 Unicode 标准。在 Go 的术语中,这些字符记号称为文字符号(rune),rune 类型实际上就是 int32 类型的别名。
var str = "hello 你好"
// go 中 string 底层是通过 byte 数组实现的,直接求 len 实际是在按字节长度计算,一个汉字占3个字节
fmt.Println("len(str):", len(str))
// 以下两种都可以得到 str 的字符串长度
// unicode/utf8包提供了用utf-8获取长度的方法
fmt.Println("RuneCountInString:", utf8.RuneCountInString(str))
// 通过rune类型处理unicode字符
fmt.Println("rune:", len([]rune(str)))
// len(str): 12
// RuneCountInString: 8
// rune: 8
- byte 等同于int8,常用来处理ascii字符
- rune 等同于int32,常用来处理unicode或utf-8字符
为了避免转换和不必要的内存分配,bytes 包和 strings 包都预备了许多对应的实用函数,它们两两对应。例如:
func Contains(s,substr string) bool
func Contains(b,subslice []byte) bool
聚合类型
是通过组合各种简单类型得到的更复杂的数据类型。
数组
数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。
如果省略号“…”出现在数组长度的位置上,那么数组的长度由初始化数组的元素个数来决定。
q:=[...]int{1, 2, 4,}
fmt.Printf("%T",q)// [3]int
数组的长度是数组类型的一部分,所以[3]int 和[4]int 是两种不同的数组类型。
数组赋值也可以给出这样一组值,索引和索引对应的值,默认的索引是顺序的。没有指定值的索引元素默认被赋予数组元素类型的零值。
q := [...]int{5: 5,}
fmt.Println(q) // [0 0 0 0 0 5]
w := [10]int{5: 5,}
fmt.Println(w) // [0 0 0 0 0 5 0 0 0 0]
函数传参中,Go 把数组和其他的类型都看成值传递。而在其他的语言中,数组是隐式地使用引用传递的。但是 Go 中也可以显式地传递一个数组的指针给函数,这样在函数内部对数组的任何修改都会反映到原始数组上面。
结构体
结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型。
成员变量的访问:
- 通过点号访问。
- 获取成员变量的地址,通过指针访问。
- 在结构体指针上应用点号访问。
type people struct {
ID int
Name string
}
p := people{ID: 1, Name: "Li li",}
// 1.
fmt.Println(p.Name)
// 2.
name := &p.Name
fmt.Println(*name)
// 3.
var pointerOfPeople *people = &p
fmt.Println(pointerOfPeople.Name)
fmt.Println((*pointerOfPeople).Name)
- 成员变量的顺序对于结构体的同一性很重要。
- 聚合类型不可以包含它自己,但是可以包含自己的指针类型。
结构体字面量
结构体类型的值可以通过结构体字面量来设置,及通过结构体的成员变量来设置。
- 按照正确的顺序,为每个成员变量指定一个值。
- 通过指定部分或者全部成员变量的名称和值来初始化结构体变量,没有指定的成员变量的值就是该成员变量类型的零值。
- 创建一个 struct 类型的变量并获取它的地址。
type people struct {
ID int
Name string
}
p1 := people{1, "Li li",}
p2 := people{ID: 1, Name: "Li li",}
p3 := &people{ID: 1, Name: "Li li",}
结构体比较
如果结构体的所有成员变量都可以比较,那么这个结构体就是可比较的。
结构体嵌套和匿名成员
Go 中允许定义不带名称的结构体成员,只需要指定类型即可;这种结构体成员称为匿名成员。这个结构体成员的类型必须是一个命名类型或者指向命名类型的指针。
“匿名成员”具有隐式的名字,所以不能在一个结构体中定义两个相同类型的匿名成员。
type Point struct {
x, y int
}
type Circle struct {
Point
Radius int
}
c := Circle{Point{1, 2}, 1,}
c0 := Circle{Point: Point{1, 2}, Radius: 1,}
fmt.Printf("%#v\n", c)
fmt.Println(c.x)
fmt.Println(c0.Point.y)
// main.Circle{Point:main.Point{x:1, y:2}, Radius:1}
// 1
// 2
在 Go 中,组合是面向对象编程方式的核心。
方法
方法的声明和普通函数的声明类似,只是在函数名字前面多了一个参数。这个参数把这个方法绑定到这个参数对应的类型上。
- 附加的参数 p 称为方法的接受者,接受者不使用特殊名(this 或 self),而是自己选择的名字;
- 类型所拥有的方法名必须是唯一的,但不同的类型可以使用相同的方法名。
方法的使用:
package main
import (
"fmt"
"image/color"
"math"
)
type Point struct {
X,Y int
}
func (p Point)Distance(q Point) int {
return int(math.Hypot(float64(p.X-q.X), float64(p.Y-q.Y)))
}
func (p Point)Scale(i int) Point {
p.X*=i
p.Y*=i
return p
}
func (p *Point)ScalePointer(i int) {
p.X*=i
p.Y*=i
}
type ColorPoint struct {
Point
Color color.RGBA
}
type P *int
// 不允许本身是指针的类型进行方法声明
//func (P)f() {
//
//}
func main() {
p:=Point{1, 2,}
//1. 复制每一个实参变量,不会改变实参的值。
p.Scale(2)
fmt.Println(p)// {1 2}
fmt.Println(p.Scale(2))// {2 4}
//2. 使用指针传递变量的地址,可以对变量进行 &p 的隐式转换。
p.ScalePointer(3)
fmt.Println(p)// {3 6}
//3. 不能够对一个不能取地址的 Point 接受者参数调用 *Point 方法,因为无法获取临时变量的地址。
//Point{5, 6,}.ScalePointer(2) // 报错
//4. 可以对变量进行 *p 的隐式转换。
pPointer:=&Point{3, 4,}
fmt.Println(pPointer.Scale(2))// {6 8}
//5. 虽然 ColorPoint 内嵌了 Point,但是 ColorPoint 无法转换成 Point 类型,所以必须显示地转换。
cp:=ColorPoint{Point{1, 2,},color.RGBA{}}
cp.ScalePointer(2)
//cp.Distance(cp)// 报错
fmt.Println(cp.Distance(cp.Point))// 0
//6. 使用方法变量调用函数,只需要提供实参而不需要提供接收者就能够调用。
scaleP:=p.ScalePointer
scaleP(5)
fmt.Println(p)// {15 30}
//7. 使用方法表达式调用函数。
scaleP1:=(*Point).ScalePointer
scaleP1(&p, 3)
fmt.Println(p)// {45 90}
}
- 复制每一个实参变量,不会改变实参的值。
- 使用指针传递变量的地址,可以对变量进行 &p 的隐式转换。(不允许本身是指针的类型进行方法声明)
- 不能够对一个不能取地址的 Point 接受者参数调用 *Point 方法,因为无法获取临时变量的地址。
- 可以对变量进行 *p 的隐式转换。
- 虽然 ColorPoint 内嵌了 Point,但是 ColorPoint 无法转换成 Point 类型,所以必须显示地转换。
- 使用方法变量调用函数,只需要提供实参而不需要提供接收者就能够调用。
- 使用方法表达式调用函数。
引用类型
间接指向程序变量或状态,操作所引用数据的效果就会遍及该数据的全部引用。
指针
变量是存储值的地方,指针的值是一个变量的地址。不是所有值都有地址,但是所有的变量都有。
返回局部变量的地址是非常安全的:
func f() *int{
v:= 1
return &v
}
fmt.Println(f()==f())// false
slice
slice 表示一个拥有相同类型元素的可变长度的序列。slice 通常写成[]T,其中元素的类型都是 T,看上去像没有长度的数组类型。
slice 是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素,而这个数组称为 slice 的底层数组。一个底层数组可以对应多个 slice,这些 slice 可以引用数组的任何位置。
slice 有三个属性:指针、长度和容量。内置函数 len 和 cap 分别返回 slice 的长度和容量。
slice 无法比较,唯一的合法比较是与 nil 比较。
因为 slice 包含了指向数组元素的指针,所以将 slice 传递给函数的时候,可以在函数内部修改底层数组的元素。创建一个数组的 slice 等于为数组创建了一个别名。
内置函数 append 用来将函数追加到 slice 的后面,当 slice 无空间时,扩展 slice 的内容。所以我们不能假设原始的 slice 和 append 后的 slice 指向同一个底层数组,通常将 append 的调用结果再赋值给传入参数的 slice。
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3,}
fmt.Printf("%T\n", a) // [3]int,数组类型
b := a[:2]
fmt.Printf("%T\n", b) // []int,slice 类型
b[0] = 10
fmt.Println(a)
fmt.Println(b)
b = append(b, 1)
b = append(b, 2)
b = append(b, 3)
// 此时 b 已经不是原来的 slice,
b[0] = 100
fmt.Println(a)
fmt.Println(b)
}
map
散列表是一个拥有键值对元素的无序集合。键的值是唯一的,并且键的类型必须是可以通过==操作符来进行比较的数据类型。
内置函数 make 可以创建一个 map。
map 的下标访问操作返回两个值,第二个值是一个布尔值,表示元素是否存在,用来区分“元素不存在”和“元素存在但值为零”的情况。
map 无法比较,唯一的合法比较是与 nil 比较。
ages := make(map[string]int)
ages["bob"] = 21
if age, ok := ages["bob"]; !ok {
fmt.Println("bob not found")
} else {
fmt.Println("age: ", age)
}
if age, ok := ages["lili"]; !ok {
fmt.Println("lili not found")
} else {
fmt.Println("age: ", age)
}
函数
多返回值
一个函数能返回不至一个结果。
裸返回是将每个命名返回结果按照顺序返回的快捷方法。
func f()(a,b int,err error) {
return // return a,b,err
}
函数变量
函数可以作为变量,拥有类型,可以赋值给变量或者从其他函数中返回,函数变量还可以像其他函数那样调用。但是函数变量本身不可比较。
匿名函数
可以使用函数字面量在任何表达式内指定函数变量,这样的表达式的值是一个匿名函数。通常称为闭包。
func squares() func() int { // 返回值类型为 func() int
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // 1
fmt.Println(f()) // 4
fmt.Println(f()) // 9
fmt.Println(f()) // 16
}
变长函数
变长函数在调用的时候可以有可变的参数个数。在参数列表最后一个类型名称之前用省略号…表示声明一个变长函数。
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
变长函数的类型和带有普通 slice 参数的函数类型不同。
延迟函数调用
在函数调用前加上 defer,函数的实际调用推迟到包含 defer 语句的函数结束后才执行,按照调用 defer 的顺序倒序执行。
错误处理和宕机恢复
Go 在进行错误检查之后,检测到失败的情况往往都在成功之前,如果检测到失败则导致函数返回,成功的逻辑一般不会放在 else 块中而是会放在外层的作用域中。
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // 结束读取
}
if err != nil {
return // 错误处理
}
// use r
}
- 调用 panic 函数会引发程序宕机。
- defer 函数中延迟调用的 recover 函数会终止宕机状态并且返回宕机的值。
// 错误处理函数,必须要先声明defer,否则不能捕获到panic异常
func errorHandler() {
fmt.Println("begin error handler")
// 在if之后,条件语句之前,可以添加变量的初始化语句,使用;间隔
// recover() 可以捕获到 panic 的 error
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("end error handler")
}
func main() {
defer errorHandler()
f()
}
func f() {
fmt.Println("第一步:f()")
fmt.Println("第二步:f()")
// panic 之后,程序终止,按照 defer 的顺序逆序执行
panic("异常信息")
fmt.Println("第三步:f()")
fmt.Println("第四步:f()")
}
goroutine 和通道
Go 的 goroutine 和通道支持通信顺序进程(CSP),CSP 是一个并发的模式,在不同的执行体之间传递值,但是本身变量局限于单一的执行体。
在 Go 里,每一个并发执行的活动称为 goroutine。两个 goroutine 可以同时执行,当 main 返回时,所有的 goroutine 都被暴力终结。
通道是 goroutine 之间的连接,通道是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。通道有两个主要操作:发送和接收,通道还可以关闭。
内置函数 make 可以创建一个通道并指定其容量,容量为 0 的通道为无缓冲通道。
无缓冲通道
使用无缓冲通道进行的通信机制导致发送和接收 goroutine 同步化。因此,无缓冲通道也称为同步通道。当一个值在无缓冲通道上传递时,接收值后发送方 goroutine 才被再次唤醒。
管道
通道可以用来连接 goroutine,这样一个的输出是另一个的输入。这个叫管道。
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
// counter: 0,1,2,3...
go func() {
for x := 0; x < 10; x++ {
naturals <- x
}
close(naturals)
}()
// squarer: 0,1,4,9...
go func() {
for {
x, ok := <-naturals
if !ok {
break
}
squares <- x * x
}
close(squares)
}()
// printer
for {
x, ok := <-squares
if !ok {
break
}
fmt.Println(x)
}
}
单向通道
Go 提供了单向通道类型,只能进行发送或接收操作。
- chan<- int 是一个只能发送的通道
- <-chan int 是一个只能接收的通道
package main
import (
"fmt"
)
func counter(out chan<- int) {
for x := 0; x < 10; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
缓冲通道
缓冲通道有一个元素队列,队列的最大长度在创建的时候通过 make 的容量参数来设置。
- 如果通道已满,发送语句将会阻塞。
- 如果通道为空,接收语句将会阻塞。
接口类型
Go 的接口是隐式实现的。对于一个具体的类型,无须声明它实现了哪些接口,只要提供接口所必需的方法即可。
一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型定义中的所有方法。
实现接口
如果一个类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。
package main
import (
"fmt"
"strconv"
)
type People struct {
name string
age int
}
// 定义一个 String 方法可以让这个类型满足 fmt.Stringer 接口
func (p People) String() string {
return "name: " + p.name + ", age: " + strconv.Itoa(p.age)
}
func main() {
p := People{"bob", 21,}
fmt.Println(p)
}
空接口类型对其实现类型没有任何要求,所以可以把任何值赋给空接口类型。
var any interface{}
any = true
any = 1
接口值
一个接口类型的值(接口值)包含两个部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。
赋值把一个具体类型隐式转换为一个接口类型,这与 w = io.Writer(os.Stdout) 等价。
var w io.Writer
w = os.Stdout// w = io.Writer(os.Stdout)
w = new(bytes.Buffer)
w = nil
如果接口动态值为空,但是动态类型不为空,这时接口是不为空的,但是对于接口的操作都会因为动态值为空而失败。
类型断言
类型断言是一个作用在接口值上的操作,x.(T),其中 x 是一个接口类型的表达式,而 T 是一个类型(称为断言类型)。类型断言会检查作为操作数的 x 的动态类型是否为 T,如果检查成功,那么返回 x 的动态值。
var w io.Writer
w = os.Stdout
f := w.(*os.File) // 成功
c := w.(*bytes.Buffer) // 报错
类型分支
类型分支与普通分支语句类似,差别为操作数改为 x.(type),type 为关键词,而不是一个特定类型,每个分支也是一个或多个类型,可以针对接口的不同的动态类型执行不同的操作。
var x interface{}
switch x.(type) {
case nil: // ...
case int, uint: // ...
case bool: // ...
default: // ...
}
接口的使用
接口和三个实现的方式:
import "fmt"
//1.接口,可以放在独立文件中
type DemoInterface interface {
Demo()
Second()
}
/**
2.类似于抽象类的实现,可以放在独立文件中
可以实现一个default的结构体,里面给出所有的默认实现,类似于面向对象的abstract方法(但可以被new),可以在里面实现公共的逻辑和默认实现
*/
type AbstractImpl struct {
state bool
}
//提供默认实现
func (d *AbstractImpl) Demo() {
fmt.Println("generic Demo")
}
func (d *AbstractImpl) Second() {
fmt.Println("generic Second")
}
//提供额外的公用方法供具体实现使用
func (d *AbstractImpl) commonForSpecificImpls() {
fmt.Println("1:common code for sub-class to use")
}
//额外方法2
func (d *AbstractImpl) commonForSpecificImpls2() {
fmt.Println("2:common code for sub-class to use")
}
/**
3.具体实现,可以放在独立文件中
具体实现的类,可以只实现需要的、不同于abstract的逻辑,假如不需要Second这个方法,可以不用实现
*/
type SpecificImpl struct {
AbstractImpl //把默认实现组合进来
state bool //其他扩展信息
}
//只实现了需要的方法
func (d *SpecificImpl) Demo() {
d.commonForSpecificImpls() //在AbstractImpl实现的逻辑,直接使用
fmt.Println("specific Demo")
d.commonForSpecificImpls2()
}
/**
4.测试方法:
输出:
generic Demo
generic Second
1:common code for sub-class to use
specific Demo
2:common code for sub-class to use
generic Second
*/
func main() {
d := AbstractImpl{} //默认实现
d.Demo()
d.Second()
fmt.Println()
s := SpecificImpl{}
s.Demo()
s.Second() //默认的实现
}
go 工具
Go 除了自带的包之外,还有配套的 go 工具,一个复杂但是容易使用的命令行工具,用来管理 Go 包的工作空间。
工作空间
必须进行的唯一配置是 GOPATH 环境变量,它指定工作空间的根。当需要切换到不同的工作空间时,更新 GOPATH 变量的值即可。
GOPATH 有三个子目录:
- src:源文件
- pkg:构件工具存储编译后的包的位置
- bin:可执行程序
go env 命令输出与工具链相关的已经设置有效值的环境变量及其所设置值,还会输出未设置有效值的环境变量及其默认值。
- GOOS 指定目标操作系统,例如:android、linux、darwin 或 windows;
- GOARCH 指定目标处理器架构,例如:amd64、386 或 arm。
包的下载
go get 命令可以下载单一的包,也可以使用…符号来下载子树或仓库,该工具也计算并下载初始包所有的依赖性。
go get 创建的目录是远程仓库的真实客户端,而不仅仅是文件的副本,这样可以使用版本控制命令来查看本地编辑的差异或者更新到不同的版本。
包的构建
go build命令编译每一个命令行参数中的包。如果包的名字是 main,go build 调用连接器在当前目录中创建可执行程序,可执行程序的名字取自包的导入路径的最后一段。如果没有提供参数,会使用当前目录作为参数。
go install 和 go build 的区别是它会保存每一个包的编译代码和命令,而不是把它们丢弃。go build 和 go install 对于没有改变的包和命令不需要重新编译,从而使后续的构建更加快速。
包的文档化
go doc 工具输出在命令行上指定的内容的声明和整个文档的注释。
godoc 可以提供相互链接的 HTML 页面服务,进而提供不少于 go doc 命令的信息。
godoc -http :8000
// 访问 http://localhost:8000/pkg
包的查询
go list 工具上报可用包的信息。
// 判断一个包是否存在于工作空间中,如果存在输出它的导入路径。
go list github.com/go-sql-driver/mysql
// ... 通配符匹配包的导入路径中的任意子串
go list ...
// 获取指定的子树中的所有包
go list gopl.io/ch3/...
// 获取一个具体的主题相关的所有包
go list ...xml...
// 以 JSON 格式输出每一个包的完整记录
go list -json hash
包的测试
go test 是 Go 语言包的测试驱动程序。以“_test.go”结尾的文件不是 go build 命令编译的目标,而是 go test 编译的目标。
- 功能测试函数
- 以 Test 前缀命名的函数,用来检测程序逻辑的正确性。go test 运行测试函数,并报告结果是 PASS 还是 FAIL。
- 基准测试函数
- 以 Benchmark 前缀命名的函数,用来测试某些操作的性能。go test 报告操作的平均运行时间。
- 示例函数
- 以 Example 前缀命名的函数,用来提供机器检查过的文档。
- 如果一个示例函数最后包含一个类似这样的注释“// 输出:”,测试驱动程序将执行这个函数并且检查输出到终端的内容匹配这个注释中的文本。
go test 工具扫描 *_test.go 文件来寻找特殊函数,并生成一个临时的 main 包来调用它们,然后编译和运行,并汇报结果,最后清空临时文件。
-v 选项可以输出包中每个测试用例的名称和执行的时间。
-run 的参数是一个正则表达式,使 go test 只运行那些测试函数名称匹配给定模式的函数。
-bench 选项的参数指定了要运行的基准测试,默认情况下不会运行任何基准测试。