1

Golang细节梳理

 2 years ago
source link: http://yaoguais.github.io/article/golang/detail.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Golang细节梳理

熟话说,“工欲善其事,必先利其器”。接触Golang也近一年半,多次想梳理一下,都半途而废,这次趁着空闲,刚好可以一一记录下来。

内容多摘自《Go语言学习笔记》,或他人博文,或官方文档。形式为概括与代码结合,阐述也多以代码注释出现。

本篇文档概括至Go最新发布版,且不定更新。主要分三个方面:一是语言特性,如“类型、表达式、函数、数据、方法、接口”等; 二是编程技巧与理念,如“缓存池、用通信实现数据共享”;三是个人理解,如“对开源项目解构”。

Update 2017/10/14:

经过六天的整理,完成第一章至第九章,后面的内容还挺多的,仍需努力呐。

参考资料:

《Go语言学习笔记》

Tony Bai

鸟窝

gopherchina

  1. 常量与变量

类型包含基础类型与引用类型,基础类型包含“int、float”等等,引用类型包含“slice、map、channel”三种类型。

(1)基础类型:

  • 1字节的:bool、byte、int8、uint8
  • 2字节的:int16、uint16
  • 4字节的:int32、uint32、rune、float32
  • 8字节的:int64、uint64、float64、complex64
  • 16个字节的:complex128

跟32位、64位平台相关的有:

  • 4或8字节:int、uint、uintptr

额外的基础类型还有:string、array、struct、function、interface。

(2)引用类型:

  • slice
  • channel

引用类型有两个特点:

  1. 作为参数作为返回值时,不发生内存拷贝。因此基础类型array是被发生拷贝的,大数组作为参数应该转换为slice。
  2. 使用前应用make进行初始化,用new只会分配返回的指针的内存。如map就不会分配其附加属性count等,也不会正确初始化。

(3)类型转换与别名

golang中必须显式类型转换,不同于C/C++。 但是如果是别名的话,因为本身就是同一种类型,因为可以直接赋值。 1.9版本更是增加了定义别名特性。

内置的别名有:

  • type byte = uint8
  • type rune = uint32

类型转换中容易造成歧义,需要使用括号消除歧义,或使阅读更加清晰。如:

(*int)(p) 转换为指针。但*int(p) 即为*(int(p)),意为取地址p处的值,这种用法在操作系统管理内存时多用。

(4)自定义类型

  • type color uint8

自定义类型不是别名。它是从现有的类型定义出一个新的类型,它们已是两种类型,只是拥有相同的底层数据结构。 其区别有以下几点:

  • 不拥有原类型的“method set”,即不实现原类型已实现的接口
  • 同原类型必须显式转换
  • 同原类型不能直接比较

(5)未命名类型

array、slice、map、channel因为与其元素具体类型或长度有关,所以不能给出唯一的名称,固成为未命名类型。

像数组[2]int即是,但我们也可以给他取一个名称。

type data [2]int

未命名类型转换为有名类型的规则如下:

  • 所属的类型相同
  • 基础类型相同,其中一个是未命名类型。
  • 数据类型相同,将双向通道赋值给单向通道,其中一个为未命名类型。
  • 将nil赋值给slice、map、channel、指针、函数或接口。
  • 将对象赋值给接口变量,只要对象实现了该接口。

(6)默认值

当声明一个变量时,系统都被默认初始化,以确保不会发生异常。这不同于C的局部变量。

其总结如下:

  • false:bool
  • 0:int族、uintptr
  • 0.0:float族
  • 空字符串"":string
  • nil:function、interface、slice、map、channel

array、struct根据其元素不同而对应赋予默认值。

(7)类型推断

类型推断就是不用声明具体的类型,而根据被赋予的值推断类型。 所以我们在使用类型推断时,应该清楚的知道当前被赋予什么类型。

var1 := 0
var2 := 0.0
// var3 := 0xffffffffffffffff //constant 18446744073709551615 overflows int
var4 := 'c'
var5 := '我'
fmt.Printf("0: %T\n", var1)
fmt.Printf("0.0: %T\n", var2)
// fmt.Printf("0xffffffffffffffff: %T\n", var3)
fmt.Printf("c: %T\n", var4)
fmt.Printf("我: %T\n", var5)

//output:
0: int
0.0: float64
c: int32
我: int32

可以看出整数是推断出int,浮点数是推断出float64,字符是推断出int32,如果超出其范围,在编译时就会报错。

常量与变量

(1)常量

常量固不能改变的量。其值必须是编译时期可以确定的字符、字符串、数字或布尔值。

其有以下几个特点:

  • 其值必须是字符、字符串、数字、布尔值
  • 支持类型推断

在声明时,可以使用常量组,如果不指定类型和初始值,则与上一行非空常量右值表达式文本相同。 这里提到表达式文本,主要是针对iota而言的,因为处于不同行的iota的值是不相同的。

常量的值也可以是编译时期可以确定的表达式,其例子如下:

const (
    ptrSize = unsafe.Sizeof(uintptr(0))
    strSize = len("string")
    sliceCap = cap(sliceA)
)
  • 常量也可以在不同作用域定义同名的常量而不会冲突。

(2)枚举

在Golang中没有内置枚举类型,但是我们一般是通过常量配合iota实现的。

iota有如下几个特点:

  • 作用于常量组,不同常量组的iota的初始值都是0
  • 常量组每增加一行,不管是否包含iota,iota的值都自增1
  • 处于同一行的iota,其值也是相同的
  • iota的默认类型的int,也可显示指定为其他类型,如float32等

(3)变量

变量使用var关键字定义,如果未给初始值系统会初始化为默认值。

变量支持简短模式,即使用操作符“:=”,其有以下三个限制:

  • 定义变量需要同时显示初始化
  • 不能声明其数据类型
  • 只能用在函数内部,即不能作用于全局变量

变量支持退化赋值,其条件为:至少有一个新变量被定义,新变量须与旧变量处于统一作用域。

局部变量未使用会在编译时报错。但全局变量和常量不会有这个问题。

(4)多变量赋值

赋值时先从左至右依次计算右值,然后再依次赋值给左边的变量。

如下面这种情况:

func inc(x *int, name string) int {
        fmt.Printf("%s: %d\n", name, *x)
        *x++
        return *x
}

func main() {
        x, y := 1, 3
        x, y = inc(&y, "y"), inc(&x, "x")
        fmt.Printf("x=%d, y=%d\n", x, y)
}

// output:
y: 3
x: 1
x=4, y=2

(5)空标识符“_”

空标识符多用在接收多返回值时,忽略其中几个返回值。当然忽略error不是一个好习惯。

另一个常用的地方是检测某类型是否实现目标接口。如:

var _ InterfaceA = new(StructA)

(6)作用域

变量需要特别关注作用域,当多个同名变量出现时,一定要清楚改变的谁,谁又不变。

var x int
for x, y := 1, 1; x < 5; x, y = x+1, y+1 {
        fmt.Printf("%d ", x)
}
fmt.Printf("\nx=%d\n", x)
// output:
1 2 3 4
x=0

注意,Golang中没有逗号表达式,即不能使用“i++, j++”这样的语句。
解决办法就是前面提到的多变量赋值。

表达式这部分包含八部分:保留字、运算符、优先级、位运算符、二元运算符、自增、指针运算、控制流。

(1)保留字

如果你还不清楚一种语言的任何一个保留字,那么说明你还没有彻底理解这种语言。

Golang目前的保留字有25个,我们如下分类:

  • 用作定义的:const、var、type、struct、interface、map、chan、func
  • 用作包管理的:package、import
  • 用作控制流的:if、else、switch、case、for、goto、continue、break、fallthrough、default、select
  • 特殊的:go、defer、range、return

除上面这些保留字外,还有一些特殊的关键字,如len、cap。

(2)运算符

运算符除了加减乘除和各种括号外,一个比较的特别的是“&^”,我们叫它“位清除符”, 它将左右操作数都为1的位清除为0。

(3)指针运算

Golang中与指针相关的有取址运算符“&”、指针运算符“*”、类型uintptr、类型unsafe.Pointer。

指针可以支持相等运算符,但不能像C语言一样加减乘除和一般类型转换。

其拥有以下几种特殊操作:

  • 任意类型的指针可以被转换成一个 Pointer对象。
  • 相反一个Pointer也可以转换成任意类型的指针。
  • 一个uintptr可以转换成一个Pointer。
  • 相反一个Pointer可以转换成uintptr。
  • 内建的new函数可以为类型T创建零值的对象,它返回的对象类型为*T。

unsafe.Pointer定义如下:

type ArbitraryType int  // shorthand for an arbitrary Go type; it is not a real type
type Pointer *ArbitraryType

示例代码如下:

// 移动指针
{
        x := [...]int{1, 2, 3, 4, 5}
        p := &x[0]
        //p = p + 1
        index2Pointer := unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(x[0]))
        p = (*int)(index2Pointer) //x[1]
        fmt.Printf("%d\n", *p)    //2
}
// 任意类型的指针可以被转换成一个 Pointer对象
// *T -> Pointer to T2
{
        var i int64 = 100
        var p *int64 = &i //*int64
        P := unsafe.Pointer(p)
        fmt.Printf("%T\n", P)
}
// 相反一个Pointer也可以转换成任意类型的指针
// Pointer to T2 -> *T
{
        var i int64 = 100
        var p *int64 = &i //*int64
        P := unsafe.Pointer(p)
        p2 := (*int32)(P) //*int32
        fmt.Println(*p2)
}
// 一个uintptr可以转换成一个Pointer,相反一个Pointer可以转换成uintptr
{
        var i int64 = 200<<32 + 100
        var p = &i
        P0 := unsafe.Pointer(p)
        P := unsafe.Pointer(uintptr(P0) + 4)
        p2 := (*int32)(P)
        fmt.Println(*p2) //200
}
// 内建的new函数可以为类型T创建零值的对象,它返回的对象类型为*T
{
        var i = new(int)
        var s = new(string)
        var j = new(struct{ int })
        fmt.Printf("%T %T %T\n", i, s, j) //*int *string *struct { int }
}
// output:
2
unsafe.Pointer
100
200
*int *string *struct { int }

http://colobu.com/2016/06/16/dive-into-go-3/

(4)控制流

Golang用作控制流的保留字,已经在上面例举出来。

控制流也要特别关注变量的作用域,像上面已经列举的for语句,除外还有if、else、switch、select。

这里要提到goto,遇到很多人见到goto就一棒子打死,结果本来goto写出来很干净的代码,非要层层判断导致既冗余又难读,何必呢?

(5)switch

switch在golang中比较特殊,我们单独阐述。其基本结构如下:

switch 条件表达式 {
    case 比较表达式: 语句
    case 比较表达式2: 语句
    default: 语句
}

其有如下特点:

  • 条件表达式可以为空,这时其值为true。
  • 条件表达式支持初始化语句,如switch x := 5; x {}。
  • 比较表达式可以有多个值,如case 5, 6,用逗号隔开即可。
  • 每一个比较表达式的语句,最后会自动break。要继续执行紧挨着的下个case或default,需要使用fallthrough。
  • 匹配规则是从上向下,从左向右。从左向右是因为比较表达式支持多个值。
  • 比较表达式可以是布尔值、常量、变量、或逻辑表达式,或多个用逗号隔开的组合。
  • 当所有比较表达式都匹配之后,才会执行default,而且不管default是否放到最后,但一般都放最后。

(6)select

select多用在选择多个通道,或结合default来尝试读写通道。

其结构与switch类似:

select {
case <-a:
case <-b:
case <-a:
default:
}

当有多个通道选择时,其具有下面的特点:

  • 它会随机选择一个可用的通道,即便是同一通道。
  • 若将通道设置成nil, 则通道便不会被选中。
  • 当所有通道都不可用时,才会执行default的语句。如果没有default,那么select会一直等待通道可用。
  • 关闭通道后,通道会一直可读,但是读出来的是nil。要避免select空跑。

(7)break

break用作跳出for、switch或select。

一个特殊用法是结合标签跳出多层循环,如:

func main() {
Exit:
        // fmt.Println("generate invalid break label")
        for i := 0; i < 5; i++ {
                for j := 0; j < 5; j++ {
                        fmt.Println(i, j)
                        if i+j >= 4 {
                                break Exit
                        }
                }
        }
        fmt.Println("end")
}
// output:
0 0
0 1
0 2
0 3
0 4
end

(8)for...range

for...range主要完成迭代,支持字符串、数组、数组指针、切片、字典、通道。

在遍历时的局部变量,也会被重复使用,因而其地址不变。

示例代码如下:

func main() {
        fmt.Println("普通字符串")
        for i, v := range "abc" {
                fmt.Printf("i=%v, v=%v\n", i, v)
        }
        fmt.Println("汉字字符串")
        for i, v := range "汉字" {
                fmt.Printf("i=%v, v=%v\n", i, v)
        }
        fmt.Println("数组")
        for i, v := range [2]int{10, 11} {
                fmt.Printf("i=%v, v=%v\n", i, v)
        }
        for i := range [2]int{10, 11} {
                fmt.Printf("i=%v\n", i)
        }
        fmt.Println("切片")
        for i, v := range []int{10, 11} {
                fmt.Printf("i=%v, v=%v\n", i, v)
        }
        fmt.Println("字典")
        m := make(map[string]string)
        m["a"] = "b"
        for k, v := range m {
                fmt.Printf("k=%v, v=%v\n", k, v)
        }
        for k := range m {
                fmt.Printf("k=%v\n", k)
        }
        fmt.Println("通道")
        c := make(chan int)
        go func() {
                for v := range c {
                        fmt.Printf("v=%v\n", v)
                }
                fmt.Println("read channel finish")
        }()
        c <- 20
        c <- 21
        close(c)
        // c = nil // 会导致for-range阻塞,从而可能导致死锁。
        fmt.Println("局部变量会重复使用")
        for i, v := range [2]int{10, 11} {
                fmt.Printf("i=%v, v=%v\n", &i, &v)
        }

        done := make(chan struct{})
        <-done
}
// output:
普通字符串
i=0, v=97
i=1, v=98
i=2, v=99
汉字字符串
i=0, v=27721
i=3, v=23383
数组
i=0, v=10
i=1, v=11
i=0
i=1
切片
i=0, v=10
i=1, v=11
字典
k=a, v=b
k=a
通道
v=20
局部变量会重复使用
i=0xc420014138, v=0xc420014140
i=0xc420014138, v=0xc420014140
v=21
read channel finish
fatal error: all goroutines are asleep - deadlock!

遍历数组指针,其实就是在遍历其指向的数组。如:

a := [2]int{10, 11}
p := &a
fmt.Printf("%T\n", p)
for i, v := range p {
        fmt.Printf("i=%v, v=%v\n", i, v)
}
// output:
*[2]int
i=0, v=10
i=1, v=11

在遍历数组时,实际遍历的是数组的复制品,所以会发生内存拷贝。可以转换为切片再遍历。 比如:

data := [3]int{10, 20, 30}
for i, x := range data { // 这里有内存拷贝
}
// 不会拷贝内存
for i, x := range data[:] {
}

Golang内置的数据类型我们已经在“类型”一章已经梳理。这部分重点讲解string、array、slice、map。 像struct我们单独放一章。

(1)字符串

字符串是不可变字符序列,在golang中的定义类似于[]byte+length。

其有一下特点:

  • 不同于C的字符串,结尾没有NULL。而用额外字段length保存内存长度,因而是二进制安全的。
  • 默认以UTF-8存储,但是支持十六进制、八进制、UTF编码,如"\x61\142\u0041"。
  • 可用内置函数len获取长度,但不支持使用cap函数。
  • 默认值是空字符串"",而不是nil。
  • 可以使用命令提示符“`”,实现跨行字符串。
  • 支持比较符,支持“+、=+”。
  • 支持用下标获取元素,元素类型为uint8,如“str[0]”。
  • 不支持设置元素,如“str[0] = 'A'”会报错。
  • 使用for...range遍历可以获取Unicode字符,比如从字符串“好的”获取“好”出来。可以参考下面代码。
  • 要修改字符串,可以将其转换为[]byte或[]rune,修改后再转换回来。但是一定会重新分配内存。
  • 可用append函数将字符串添加到[]byte中,如“bs = append(bs, "abc"...)”。
  • 拼接多个字符串,可用bytes.Buffer提升性能。
  • 拼接字符串,可以考虑在栈上先分配一个大字节数组,避免垃圾回收,从而提升性能。
  • 标准库“utf8”已提供方法用作判断Unicode字符串是否合法和获取其长度。

实现字符串截取:

s := "abcdefg"
s1 := s[:3]
s2 := s[1:4]
s3 := s[2:]
fmt.Printf("%s %s %s\n", s1, s2, s3)
fmt.Printf("%#v\n", (*reflect.StringHeader)(unsafe.Pointer(&s)))
fmt.Printf("%#v\n", (*reflect.StringHeader)(unsafe.Pointer(&s1)))
// s1[0] = 'A'
// fmt.Printf("%s %s %s\n", s1, s2, s3)

// output:
abc bcd cdefg
&reflect.StringHeader{Data:0x4b9008, Len:7}
&reflect.StringHeader{Data:0x4b9008, Len:3}

使用for...range遍历Unicode字符串:

s := "汉字a"
for i := 0; i < len(s); i++ {
        fmt.Printf("%d %c\n", i, s[i])
}
for i, v := range s {
        fmt.Printf("%d %c\n", i, v)
}
// output:
0 æ
1 ±
2 
3 å
4 ­
5 
6 a
0 汉
3 字
6 a

因为字符串和[]byte转换会涉及内存拷贝,可以取巧地使用unsafe.Pointer进行转换而不发生内存拷贝。

package main

import (
        "fmt"
        "reflect"
        "unsafe"
)

func SliceToString(bs []byte) string {
        return *(*string)(unsafe.Pointer(&bs))
}

func StringToSlice(s string) []byte {
        sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
        sh.Cap = len(s)
        return *(*[]byte)(unsafe.Pointer(sh))
}

func main() {
        bs := []byte("hello")
        arr := [2]byte{'a', 'b'}
        fmt.Println(SliceToString(bs))
        fmt.Println(SliceToString(arr[:]))
        fmt.Printf("%#v\n", StringToSlice("hello"))
}
// output:
hello
ab
[]byte{0x68, 0x65, 0x6c, 0x6c, 0x6f}

(2)数组

数组主要关注其声明和赋值,其常见形式如下:

// 声明
// 声明时,每一纬长度都要明确,要不然就声明成切片了。
var a [2]int
var b [2][2]int
var c [2]int
var d [2][2][2]int
// 初始化
e := [4]int{1, 2}
f := [4]int{10, 3: 13}
g := [...]int{1, 2, 3} // 自动推导长度
h := [...]int{10, 3: 13}
i := [...]int{10, 3: 13, 14}
type user struct {
        name string
}
j := [...]user{
        {"Tom"},
        {name: "Jack"},
}
k := [2]*user{
        &user{"Tom"},
        &user{"Jack"},
}
// 赋值
a = [2]int{1}
b = [2][2]int{1: {1: 1}}

fmt.Println(a, b, c, d, e, f, g, h, i, j, k)

综合来看数组有以下特点:

  • 声明时,每一纬长度都要指明。
  • 定义时,只有第一维能使用“...”进行推断。
  • 内置函数len和cap都返回第一维长度
  • 如果元素都支持“==或!=”比较符,那么数组也可以比较,否则会报错。
  • 区别数组指针“[2]int”和指针数组“[2]int”。
  • 数组作为参数或者赋值都会导致内存拷贝,解决办法是转换为切片或者指针。

(3)切片

切片是数组的包装。其还有一个指向底层数据的指针,和len、cap两个属性。

其声明和初始化如下:

// 声明
var a []int
var b []int
var c []int
// 初始化
a = make([]int, 0, 5) // len, cap
b = make([]int, 5)    // len, cap默认等于len
c = append(c, 1)
d := []int{10, 3: 13}
// 截取
e := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
f := e[:]
g := e[1:2]
h := e[1:2:6]
i := e[:1:3]
// i := e[1::3] // 语法错误。按1个位置都可以忽略共7种可能,只有这种不行从而有6种可行。
// 其语法含义为[start:end:max]; len = end - start; cap = max - start; cap >= len;
fmt.Println(a, b, c, d)
fmt.Println(e)
fmt.Printf("%#v len:%d cap:%d\n", f, len(f), cap(f))
fmt.Printf("%#v len:%d cap:%d\n", g, len(g), cap(g))
fmt.Printf("%#v len:%d cap:%d\n", h, len(h), cap(h))
fmt.Printf("%#v len:%d cap:%d\n", i, len(i), cap(i))
// output:
[] [0 0 0 0 0] [1] [10 0 0 13]
[0 1 2 3 4 5 6 7 8 9]
[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} len:10 cap:10
[]int{1} len:1 cap:9
[]int{1} len:1 cap:5
[]int{0} len:1 cap:3

切片还有以下特点:

  • 不像数组,它不支持比较操作,仅可判断是否为nil。
  • 可以使用“s[:]”进行截取。

  • 支持append函数,append函数从第二个参数开始,都应是切片的元素。
  • 支持copy(起始地址,新增内容)函数。

(4)字典

字典即为哈希表。 字典要求key必须能够比较,以便在哈希冲突时解决冲突。 如数字、字符串、指针、某些数组、结构体,某些接口。

其有如下特点:

  • 因为是引用类型,需要用make初始化。
  • 获取不存在的key,返回val的默认值。最好使用ok-idiom模式,“v, ok =:= m["k"]”。
  • for...range每次遍历,生成的key的序列是不同的。即遍历访问key是随机的。
  • 可以使用len获取元素个数,但不支持cap函数。
  • 字典是非线程安全的,并发读写或删除都会导致错误。
  • nil字典不可写,但可以读,只是key都是不存在的。
  • 一般为避免重新哈希,需要多少个元素,应创建字典时就指定好。如“m:=make(map[string]string, 1024)”。
  • 字典不会收缩内存,删除大部分key后,可考虑创建新字典以释放内存。
  • Go1.9新增sync.Map作为线程安全的字典,其性能较高,但因为不是内置类型,使用上需当自定义类型使用。

结构体将不同类型的字段组合成一个复合类型。

常见的声明定义初始化如下:

type node struct {
        _    int
        _    int
        val  int
        next *node
}
type user struct {
        name string
        age  byte
}
// u1 := user{"Tom"} // error: too fewer values
u1 := user{"Tom", 16}
u2 := user{name: "Tom"}

u3 := struct {
        name string
        age  byte
}{
        name: "Tom",
        age:  20,
}
type file struct {
        name string
        attr struct {
                owner int
                perm  int
        }
}
f := file{
        name: "a.txt",
        //attr: {
        //  owner: 1,
        //  perm: 0755,
        //},
}
f.attr.owner = 1
f.attr.perm = 0775

type attr struct {
        perm int
}
type File struct {
        name string
        attr
}
f2 := File{
        name: "a.txt",
        attr: attr{
                perm: 0755,
        },
}
f2.perm = 0775

type FILE struct {
        name string
}
type log struct {
        name string
}
type data struct {
        FILE
        log
}
g := data{}
// g.name = "go" // error
g.log.name = "go"
fmt.Println(u1, u2, u3, f, f2, g)

综合上面,结构体具有以下特点:

  • 可用“_”作为字段名进行占位,多用于补齐内存布局。
  • 如果结构体只用一次,可以声明为匿名结构体。
  • 空结构struct{}多用在通道,作为通知作用,因为其占内存最小。
  • 结构体支持匿名字段,当匿名字段有属性重名时,需显式读写。
  • 字段标签也是结构的一部分。Go1.9后,只有标签不同的结构体可以相互赋值。

函数是特定功能的最小单元。Go中使用func定义函数。

(1)定义及特点

其定义如下:

func someFunc(int a, int b) (c int, err error) {
}
带变长参数函数:
func format(s string, a ...interface{}) (string, error){
}

函数具有以下特点:

  • 仅能判断函数是否为nil, nil也为函数的默认值。不支持比较操作。
  • 通道也可传递函数,包括匿名函数。
  • 支持匿名函数。支持闭包。
  • 支持延迟调用defer,其顺序是前进后出,构成延迟调用栈。
  • 错误处理中,error多为最后一个返回值。如“func some() (int, error){}”。
  • 没有try/catch,而使用panic/recover。

(2)参数

函数参数有以下特点:

  • 参数不支持默认值,命名支持“_”。紧邻的参数类型相同,可只声明最后一个参数类型。
  • 传递参数都是值拷贝,只不过拷贝引用类型,可以通过形参修改实参。
  • 在调用变参函数时,可以使用“...”展开切片,如“format("%d %d", []interface{}{1, 2}...)”。

传参,是传递值还是指针?

  • 值传递主要消耗在拷贝,但内存是分配在栈上的。
  • 指针传递可能导致堆上的内存分配。
  • 重点还是在拷贝成本,高则指针,不高则值。
// 指针参数导致实参重新被分配到堆上

func test(p *int) {
        go func() {
                println(p)
        }()
}

func main() {
        x := 0
        p := &x
        test(p)
}

(3)返回值

Golang是支持多返回值的,但常用在返回错误了。

其有以下几个特点:

  • 函数返回局部变量是安全的。不同于C语言。
  • 返回内容可以通过返回值,也可通过耳机指针参数,在C语言中常用。
  • 返回值可以使用“_”忽略。
  • 多返回可直接传入函数参数,不必展开。如“a(int,int){};b(int,int){};a(b());”。
  • 命令返回值,多用于生成doc。它也是函数内部的局部变量,可以随时修改。
  • 命令返回值,最好要么全部命名,全部不命名,不然return时会导致错误。

(4)匿名函数

匿名函数与一般函数无异,系统会自动给它分配一个隐式的名称。

其有以下特点:

  • 未被使用的匿名函数,会导致错误。
  • 可以赋值给变量,作为参数,作为返回值,作为结构体字段,通过通道传递。

常见的使用方式如下:

func exec(f func()) {
        f()
}

func add(a, b int) int {
        return a + b
}

type FuncAdd func(int, int) int

func returnFunc() FuncAdd {
        return func(a, b int) int {
                return a + b
        }
}

func main() {
        f := func(a, b int) int {
                return a + b
        }
        println(f(1, 2))

        exec(func() {
                fmt.Println("as a parameter")
        })

        println(returnFunc()(1, 2))

        type data struct {
                x int
                f func(int, int) int
        }

        d1 := data{
                x: 1,
                f: func(a, b int) int {
                        return a + b
                },
        }
        println(d1.f(1, 2))
        d2 := data{
                x: 1,
                f: add,
        }
        println(d2.f(1, 2))

        c := make(chan func(int, int) int, 1)
        c <- func(a, b int) int {
                return a + b
        }
        f3 := <-c
        println(f3(1, 2))
}

(5)defer

defer常用在资源释放、释放锁、错误处理等。其顺序是前进后出,构成延迟调用栈。

defer也会有一定的性能损耗,因为它它需要执行注册、执行等操作。

可以理解为defer是插入在函数退栈指令前的指定片段,当然也就在return语句的指令后面了。

func test() (z int) {
        defer func() {
                println("defer", z)
                z += 1
        }()
        return 2
}

func main() {
        println("return", test())
}
// output:
defer 2
return 3

(6)panic/recover

它们的定义如下:

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

其有如下特点:

  • 可以使用recover捕获panic提交的错误对象,panic提交什么对象,recover原样接收,必要时需要进行类型转换。
  • panic会中断函数调用,忽略panic函数后面的语句,而执行延迟调用。 未命名的返回值,一定返回它的默认值。
  • panic像其他语言一样,一直沿函数调用栈把异常提交到main函数,如果main函数也未捕捉,那么进程奔溃。
  • recover必须放到函数体中,且必须和defer配合。否则不能catch住,而导致进程崩溃。
  • defer里面再次panic,不会影响后续defer。相当于再次抛出异常,从而继续按函数栈回溯或被后面的defer捕获。
  • 没有异常或非正确的recover,其返回值是interface{} nil。
  • 连续的panic,仅最后一个会被recover捕获。

参考代码如下:

func test(n int) int {
        defer func() {
                if err := recover(); err != nil {
                        fmt.Printf("err: %#v\n", err)
                }
                n++
                fmt.Printf("after recover %v\n", n)
        }()
        n++
        panic("cause panic")
        n++
        fmt.Printf("after panic %v\n", n)
        return n
}

func testReturn() (n int) {
        defer func() {
                if err := recover(); err != nil {
                        fmt.Printf("err: %#v\n", err)
                }
                n++
                fmt.Printf("after recover %v\n", n)
        }()
        n++
        panic("cause panic")
        n++
        fmt.Printf("after panic %v\n", n)
        return n
}

func testParamInt() {
        defer func() {
                if err := recover(); err != nil {
                        fmt.Printf("err: %T %#v\n", err, err)
                }
        }()
        panic(1)
}

func testParamString() {
        defer func() {
                if err := recover(); err != nil {
                        fmt.Printf("err: %T %#v\n", err, err)
                }
        }()
        panic("hello")
}

func testParamFunc() {
        defer func() {
                if err := recover(); err != nil {
                        fmt.Printf("err: %T %#v\n", err, err)
                }
        }()
        panic(func() int {
                return 1
        })
}

func main() {
        fmt.Printf("main test %v\n", test(1))
        fmt.Printf("main testReturn %v\n", testReturn())
        testParamInt()
        testParamString()
        testParamFunc()
}
// output:
err: "cause panic"
after recover 3
main test 0
err: "cause panic"
after recover 2
main testReturn 2
err: int 1
err: string "hello"
err: func() int (func() int)(0x489f00)

recover一定要放到defer的函数体中,否则无法生效。但是defer的语句依然会正常执行。

func main() {
        defer println("recover", recover())
        panic("error come")
        println("ok")
}
// output:
recover (0x0,0x0)
panic: error come

goroutine 1 [running]:
main.main()

defer中再次抛出异常。

func test() {
        defer func() {
                fmt.Printf("test recover '%v'\n", recover())
                panic("test panic in defer")
        }()
        panic("test panic")
}

func main() {
        defer func() {
                fmt.Printf("main recover '%v'\n", recover())
        }()
        test()
}
// output:
test recover 'test panic'
main recover 'test panic in defer'

连续的panic。

func main() {
        defer func() {
                fmt.Printf("first defer recover '%v'\n", recover())
        }()
        defer func() {
                for i := 0; i < 3; i++ {
                        fmt.Printf("recover in for '%v'\n", recover())
                }
                panic("defer with for")
        }()
        defer func() {
                panic("last defer")
        }()
        panic("in main")
}
// output:
recover in for 'last defer'
recover in for '<nil>'
recover in for '<nil>'
first defer recover 'defer with for'

闭包是包含自由变量的代码块。在Golang中多是包含自由变量的函数。

其有以下特点:

  • 自由变量在函数中被使用,外部能直接修改。类似于传递的指针。
  • goroutine会因为“延迟执行”,而立即计算并复制执行的参数。
var globN int

func addGlobN() int {
        globN++
        return globN
}

func test1() []func() {
        var s []func()
        for i := 0; i < 2; i++ {
                s = append(s, func() {
                        fmt.Printf("test1 %p %v\n", &i, i)
                })
        }
        return s
}

func test2() []func() {
        var s []func()
        for i := 0; i < 2; i++ {
                x := i
                s = append(s, func() {
                        fmt.Printf("test2 %p %v\n", &x, x)
                })
        }
        return s
}

func main() {
        for _, f := range test1() {
                f()
        }
        for _, f := range test2() {
                f()
        }
        {
                a := 1
                go func() {
                        fmt.Printf("go a=%v\n", a)
                }()
                a++
                fmt.Printf("main a=%v\n", a)
        }
        {
                b := 1
                func() {
                        b++
                        fmt.Printf("func b=%v\n", b)
                }()
                b++
                fmt.Printf("main b=%v\n", b)
        }
        {
                go func(n int) {
                        fmt.Printf("go n=%v\n", n)
                }(addGlobN())
                fmt.Printf("main globN=%v\n", addGlobN())
        }
        time.Sleep(2 * time.Second)
}
// output:
test1 0xc4200120b0 2
test1 0xc4200120b0 2
test2 0xc4200120d0 0
test2 0xc4200120d8 1
main a=2
func b=2
main b=3
main globN=2
go a=2
go n=1

方法是有名类型绑定的函数。

(1)定义

除内置的“int、string”等外,可以为任何有名类型定义绑定方法。

一般定义如下:

type N int
func (x N) toString() string {
    return fmt.Sprintf("%v", x)
}

(2)匿名字段

可以直接调用结构体中匿名字段的方法。 但是如果结构体也绑定同样的方法,直接调用会调用结构体的方法。若想调用匿名字段的方法,需写清楚字段名,如“f.log.ToString()”。

(3)方法集

类型的方法集,决定其是否实现某一接口。

其有如下规则:

  • 类型T方法集包含所有receiver T的方法。
  • 类型T包含所有receiver T + T方法。
  • 匿名嵌入S,T还包含所有receiver S的方法。
  • 匿名嵌入S,T还包含所有receiver S + S方法。
  • 匿名嵌入S或S,T还包含所有receiver S + *S方法。

可见,匿名字段即为方法集而精心设计的。

(4)类型或类型指针

在定义方法时,是使用T还是*T时,有以下几个区别:

  • 造成类型的方法集不同。
  • 调用方法时,会发生内存拷贝,那么类型无法修改T的数据,*T由于是拷贝指针而可以修改。
  • 下面讲到接口,做类型转换同样有T无法修改数据而*T可以的区别。

其实我们在调用时,把T或*T当成传入函数的第一个参数,就好理解了。发不发生拷贝,就看其是不是指针或引用类型。

type N int

func (n N) value() {
        n++
        fmt.Printf("%p %v\n", &n, n)
}

func (n *N) pointer() {
        *n++
        fmt.Printf("%p %v\n", n, *n)
}
func main() {
        var n N = 1
        n.value()
        fmt.Printf("%p %v\n", &n, n)
        n.pointer()
        fmt.Printf("%p %v\n", &n, n)
}
// output:
0xc4200120b8 2
0xc4200120b0 1
0xc4200120b0 2
0xc4200120b0 2

那么如何选择使用T还是*T,大致有如下的规则:

  • 要修改实例的状态,用*T。
  • 无需修改状态的小对象或固定值,用T。
  • 大对象建议用*T,以减少内存拷贝的成本。
  • 引用类型、字符串、函数等指针包装对象,直接使用T。
  • 若包含Mutex等同步字段,用*T,以避免用T时内存拷贝,造成无效的锁操作。
  • 其他无法确定的情况,建议使用*T。

总之就是一句话,不知道选哪个的时候,用*T就行了。

(5)方法作为表达式

就像我们在C语言中,能把函数名赋值给变量一样。Golang中也可以把函数和方法赋值给变量,同样可以用变量实现函数调用。

表达式分为两种,一种是通过类型引用,叫“method expression”; 另一个种通过实例引用,叫“method value”。

通过类型引用:

type N int

func (n N) value() {
        n++
        fmt.Printf("%p %v\n", &n, n)
}

func (n *N) pointer() {
        *n++
        fmt.Printf("%p %v\n", n, *n)
}

func main() {
        var n N = 1
        fmt.Printf("%p %v\n", &n, n)

        f1 := N.value
        f2 := (*N).value
        //f3 := N.pointer // N.pointer undefined (type N has no method pointer)
        f4 := (*N).pointer

        f1(n)
        fmt.Printf("%p %v\n", &n, n)
        f2(&n)
        fmt.Printf("%p %v\n", &n, n)
        // f3(&n)
        // fmt.Printf("%p %v\n", &n, n)
        f4(&n)
        fmt.Printf("%p %v\n", &n, n)

        N.value(n)
        fmt.Printf("%p %v\n", &n, n)
        // (*N).pointer(n)
        // cannot use n (type N) as type *N in argument to (*N).pointer

}
// output:
0xc4200120b0 1
0xc4200120d0 2
0xc4200120b0 1
0xc4200120e8 2
0xc4200120b0 1
0xc4200120b0 2
0xc4200120b0 2
0xc420012110 3
0xc4200120b0 2

从上面的输出可以看出以下几点:

  • 方法的第一个参数需要与声明的类型一致,*T就必须传入指针,否则编译报类型不匹配。
  • 不管使用T还是*T,只要调用的是T的方法,都会发生复制。
  • 因为T类型的方法集中没有pointer方法,调用自然会编译报错。
  • *T方法调用不会发生复制,从而可以修改原实例。

通过实例引用,即“method value”。由实例获取其方法,实例会立即被拷贝。 如果是基础类型,那么获取的方法就无法修改原实例数据。而引用类型由于拷贝指针,从而可以修改。

type N int

func (n N) value() {
        fmt.Printf("value %p %v\n", &n, n)
}

func (n *N) pointer() {
        fmt.Printf("pointer %p %v\n", n, *n)
}

func exec(f func()) {
        f()
}

func main() {
        {
                var n N
                p := &n
                n++
                f1 := n.value
                n++
                f2 := n.value
                n++
                fmt.Printf("main %p %v\n", p, n)
                f1()
                f2()
        }
        {
                var n N
                p := &n
                fmt.Printf("main %p %v\n", p, n)
                n++
                exec(n.value)
                n++
                exec(p.value)
        }
        {
                var n N
                p := &n
                n++
                f1 := n.pointer
                n++
                f2 := n.pointer
                n++
                fmt.Printf("main %p %v\n", p, n)
                f1()
                f2()
        }
}
// output:
main 0xc4200120b0 3
value 0xc4200120c8 1
value 0xc4200120d8 2
main 0xc4200120e8 0
value 0xc4200120f0 1
value 0xc420012100 2
main 0xc420012110 3
pointer 0xc420012110 3
pointer 0xc420012110 3

接口是多个方法的集合。

接口只有包含两种内容:函数声明和嵌入的其他接口名。而不能有常量等等。

(1)特点

  • 支持ok-idiom模式,尝试进行转换,如“v, ok := x.(fmt.Stringer)”。
  • 超集接口变量可以转换为子集接口变量,就类似于把子类对象赋值给父类对象。
  • 接口嵌入接口,不能形成循环嵌套。

(2)空接口

空接口类似于Java中的Object,任何值都可以赋予空接口。

比如我们想实现一个函数,可以处理任意类型的数据,那么我们可以把参数定义为空接口。

func printType(x interface{}) {
        switch x.(type) {
        case int:
                fmt.Println("int")
        case *int:
                fmt.Println("*int")
        case string:
                fmt.Println("string")
        default:
                fmt.Printf("%T\n", x)
        }
}

func main() {
        printType(1)
        printType("hello")
        a := 1
        printType(&a)
        b := "s"
        printType(&b)
}
// output:
int
string
*int
*string

(3)类型或类型指针

当我们把T或*T赋值给接口对象时,它们的区别如下:

  • 必然的,它们都实现了目标接口。
  • 无法修改T的数据,但是可以修改*T的。原因是赋值给接口,那么接口中保存的是T或者*T的拷贝。
type data struct {
        x int
}
// var t interface{} = data{100}
// p := &t.(data) // cannot take the address of t.(data)
// t.(data).x = 101 // cannot assign to t.(data).x
// fmt.Println(t)
a := &data{200}
var b interface{} = a
c := b.(*data)
c.x = 201
fmt.Println(a.x, c.x)
// output:
201 201

(4)是否相等

判断相等常见在判断error是否为nil,但是这里极易错误。 只有一个接口变量在类型和内容都为nil时,才为nil。

type myErr struct {
}

func (*myErr) Error() string {
        return "error"
}

func main() {
        var a interface{} = nil
        var b interface{} = (*int)(nil) // 内容是个指针
        var c, d interface{}
        type e struct {
                x int
        }
        var f interface{} = e{100}
        var g interface{} = e{100}
        var h interface{} = e{101}
        var i *myErr
        var j error = i
        fmt.Println(a == nil, b == nil, c == d, b == c, f == g, f == h, j == nil)
}
// output:
true false true false true false false

通过是用作数据共享的队列。通道分为同步通道和异步通道。

(1)创建

通道可以创建同步通道、异步通道、单发通道、单收通道。

// 同步通道
a := make(chan int)
b := make(chan int, 0)
// 异步通道
c := make(chan int, 1)
d := make(chan int, 2)
// 单发通道
var e chan<- int = a
// 单收通道
var f <-chan int = a

(2)收发规则

对于已初始化的通道:

  • 同步通道中的数据只有一个,无数据时读会阻塞。没有读操作,写入数据一定阻塞。
  • 同步通道的读操作和写操作,必须配对,且一定处于不同的goroutine中。否则一定阻塞。
  • 异步通道可以保存一个或多个数据,当通道满时写入会阻塞,当通道没有数据时读取会阻塞。

对于closed和nil通道,有以下特点:

  • 向closed通道发送数据,引发panic。
  • 向closed通道接收数据,返回已缓冲数据或nil。
  • nil通道,无论收发都会阻塞。

(3)关闭规则

  • 重复关闭通道,会引发panic。
  • 不能关闭单收通道。
  • 关闭通道,所有接收操作都可以收到通知。常用作一次性通知事件。

(4)len与cap

同步通道len和cap函数都返回0。 函数len可以获取异步通道当前已缓冲的数量,cap获取缓冲区大小。 因此可以通过这点判断通道是同步还是异步。

(5)执行顺序

通道会先执行写操作后续的指令,同步通道和异步通道都是这样的。 因此在传递指针的需要特别注意,有可能读取的是又改变之后的数据。

{
        a := 1
        p := &a
        c := make(chan *int)
        go func() {
                c <- p
                *p++
                fmt.Printf("sync channel write success, write %v\n", a)
        }()
        b := <-c
        fmt.Printf("sync channel read success, read %v\n", *b)
}
{
        n := 2
        c := make(chan int, n)
        go func() {
                for i := 0; i < n; i++ {
                        c <- 1
                        fmt.Println("async channel write success")
                        time.Sleep(time.Second)
                }
        }()
        for i := 0; i < n; i++ {
                <-c
                fmt.Println("async channel read success")
        }
}
// output:
sync channel write success, write 2
sync channel read success, read 2
async channel write success
async channel read success
async channel write success
async channel read success

(6)注意事项

  • 发送大数据,应该使用指针,避免复制。
  • 因为操作通道实际会有锁,所以可以通过把多个数据打包成一个数据发送,从而提升性能。
  • 务必避免使通道永久阻塞,从而导致死锁或goroutine内存泄漏。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK