GO语言基础plus
标签搜索

GO语言基础plus

BeforeIeave
2022-07-27 / 0 评论 / 262 阅读 / 正在检测是否收录...

1 数组、字符串、切片

1.1 数组

1.1.1 基础性质

1.数组是一种值类型,元素的值可以被修改。
2.数组的传参和赋值中,都以整体复制的方式处理。
3.数组的长度是数组类型的一部分,所以不同长度的数组是不同类型。
4.一个数组变量即表示整个数组,并不是隐式的指向第一个元素的指针,而是一个完整的值,如果数组过大的话,数组的赋值会有较大的开销,一般传递一个指向数组的指针,但数组指针不是数组。
5.数组的内置函数len()和cap()返回值都是一样的,始终对应数组类型的长度。
6.对于数组的遍历,建议使用for range,它省去了对下标越界的判断。
7.数组可以有很多类型,如数值数组、字符串数组、结构体数组、函数数组、接口数组、通道数组等。
8.长度为0的数组(空数组)在内存中并不占用空间,一般常用于强调某种特有类型的操作时避免分配额外的内存空间。(一般更倾向于用空结构体代替空数组)

//使用空数组
c1 := make(chan [0]int)
go func () {
    fmt.PrintLn("c1")
    c1<- [0]int{}
} ()
<-c1

//使用空结构体
c2 := make(chan struct{})
go func () {
    fmt.PrintLn("c2")
    c1<- struct{}{}            //struct{}表示的是类型,{}表示对应的结构体值
} ()
<-c1

1.1.2 数组的定义方式

var a [3]int                    //数组的内容全部为0
var b = [...]int{1, 2, 3}        //数组元素为1,2,3
var c = [...]int{2: 3, 1: 2}    //数组元素为0,2,3
var d = [...]int{1, 2, 4: 5, 6}    //数组元素为1,2,0,0,5,6

1.2 字符串

1.2.1 字符串的基本性质

1.Go语言字符串的底层数据对应的是字节数组,其中字符串的只读属性禁止了程序中对底层字节数组的元素的修改。
2.字符串的赋值只是复制了数据的对应地址和长度,而不会导致底层数据的复制。
3.字符串支持切片操作,不同位置的切片底层访问的是同一块内存数据
4.字符串的强转为[]byte类型的方法一般不会产生运行时的开销。
5.rune[]类型其实[]int32类型,而由于底层内存结构的差异,[]rune到字符串的转换会导致重新构造字符串。
1.2.2 字符串的底层结构

//in the reflect.StringHeader pakage
type StringHeader struct{
    Data uintptr            //存储字符串指向的底层字节数组
    Len int                    //字符串的字节的长度
}

1.3 切片

1.3.1 切片的基本性质

1.相较于字符串,它解除了只读限制,每个切片有独立的长度和容量信息。
2.赋值和传参时,是将切片头信息部分按传值方式处理(因为切片头包含底层数据的指针,所以它赋值也不会导致底层数据的复制)。
3.只有当切片底层数据指针为空时,切片本身才为nil,这个时候len和cap无效(如果其底层的数据指针为空,而len和cap均有值,则说明数据本身被损坏了)

1.3.2 切片的定义方式

var(
    a []int                    //nil切片,和nil相等,一般表示一个不存在的切片
    b = []int{}                //空切片,和nil不相等,一般表示一个空集合
    c = []int{1, 2, 3}        //含有3个元素,len 和 cap 都是3
    d = c[:2]                //有两个元素的切片,len 为2, cap 为3
    e = c[0:2:cap(c)]        //含有两个元素的切片,len = 2,cap = 3
    f = c[:0]                //含有0个元素的切片,len = 0,cap = 3
    g = make([]int, 3)        //含有3个元素的切片,len = cap = 3
    h = make([]int, 2, 3)    //含有两个元素的切片,len = 2,cap = 3
    i = make([]int, 0, 3)    //含有两个元素的切片,len = 0,cap = 3
)

1.3.3 切片的底层结构

type SliceHeader struct{
    Data uintptr
    Len int
    Cap int        //表示其指向内存空间的最大容量(对应元素的个数,非字节数)
}

1.3.4 基本操作

1.内置的泛型函数append()可以在切片尾追加N个元素。但是注意,在容量不足的情况下,此操作会重新分配内存,可能导致巨大的开销。(在开头添加元素一般都会导致内存的重新分配,会将已有的所有数组全部赋值一次)
2.copy()函数与append()函数结合可以节省创建临时切片的时间和空间。
3.要实现删除切片的操作可以灵活的使用append()和copy()两个函数结合切片的截取方法。
4.在判断一个切片是否为空的时候,一般不直接和nil做比较,而是获取它的长度判断其值是否为0来判断的。

1.3.5 避免内存泄漏

切片操作并不会复制底层的数据,而底层的数组会被保存到内存中直到它不再被引用,此种情况很可能因为内存引用导致内存泄漏,情况如下:

func FindPhoneNumber (fileName string) []byte {
    b ,_ := ioutil.ReadFile(filName)\
    return regexp.MustCompile("[0-9]+").Find(b)    //返回了内存地址的引用
}

而解决方法非常简单,将需要的内容复制一遍再返回即可:

func FindPhoneNumber (fileName string) []byte {
  b ,_ := ioutil.ReadFile(filName)\
  b = regexp.MustCompile("[0-9]+").Find(b)
  return append([]byte{}, b...)
}

对于指针有相同的改良方式:

var a []*int{...}
a = a[:len(a) - 1]        //此时被删除的最后一个元素依然被引用

//此时将被删除的元素指针设置为nil即可
var a []*int{...}
a[len(a) - 1] = nil
a = a[:len(a) - 1]

1.3.6 切片类型的强制转换

例如将[]float64转换为[]int进行比较:

import "sort"

var a = []float64{2, 4, 5, 7, 2, 1, 88, 1}

func SortFloat64FastV1(a []float64) {
    //类型的强制转换
    var b []int = ((*[1 << 20]int)(unsafe.Pointer(&a[0])))[:len(a):cap(a)]
    //以int的方式给float排序
    sort.Ints(b)
}

func SortFloatFastV2 (a []float64) {
    //通过reflact.SliceHeader 更行切片头的信息实现转换
    var c []int
    aHdr := (*reflect.SliceHeader)(unsafe.Pointer(&a))
    cHdr := (*reflect.SliceHeader)(unsafe.Pointer(&c))
    *cHdr = *aHdr
    sort.Ints(c)
}

第一种方法是通过开辟一个很大的int数组,然后通过指针重新对数组进行操作。
第二种方法是更新头部信息转换数组的类型(注意:前提要保证[]float64中没有NaN和Inf等非规范浮点数)。

2 函数、方法、接口

2.1 函数

2.1.1 可变参数

当可变参数是一个空接口类型的时候,是否解包可变参数会导致不同的结果

func main(){
    var a = []interface{}{123, "abc"}
    print(a...)        //123 abc
    print(a)        //[123 abc]
}
func print(a...interface{}) {
    fmt.Ptintln(a...)
}

第一个print传入的参数是a...等价于(解包)调用print(123, abc),而第二个是调用未解包的参数a,等价于直接调用print([]interface{}{123, "abc"})。

2.1.2 (延迟调用defer)闭包

一般匿名函数捕获了外部函数的局部变量,则我们将此种函数称为闭包(闭包对捕获的外部变量并不是以传值方式访问的,而是以引用方式!)
但是这种引用方式访问外部变量的行为会导致一些隐含问题:(在for循环内使用defer语句,差评

func main() {
    for i:= 0; i < 3; i++ {
        defer func(){fmt.Print(i)}()
    }
}
// output 333

原因:defer每次引用的都是同一个i,最后i的值变为3,则运行的defer函数均输出3。
修改的方法:每一次传入独有的变量(地址不一样)即可:

//方法1
func main() {
    for i:= 0; i < 3; i++ {
        x := i                                //生成新的变量
        defer func(){fmt.Print(x)}()
    }
}

//方法2
func main() {
    for i:= 0; i < 3; i++ {
        defer func(i int){fmt.Print(i)}(i)    //以参数的形式传递
    }
}

2.1.3 指针传递

注意到:当使用切片为参数调用函数的时候,函数内部是可以更改切片的值的,这会给我们一种传递指针的假象。本质是:切片中的底层是通过隐式指针传递的,而两个指针指向的是同一块内存区域。

2.1.4 补充

1.函数是支持递归调用的(并且不会栈溢出的错误)。
2.Go语言故意淡化了堆和栈的概念,所以我们无法分别数据是存储在哪个地方的,但是,当函数返回某个变量的地址时,编译器会将它储存在合适的地方。

2.2 方法

2.2.1 基础知识

我们通常给某一个自己定义的类型定义相应的方法(方法绑定到对象上),注意类型的定义和方法必须要在同一个包中

type Cache struct{
    m map[string]string
    sync.Mutex
}

func (p *Cache) Lookup (key string) string {        //Cache类型对象的专属方法
    p.Lock
    defer p.Unlock
    
    return p.m[key]
}

2.3 接口

2.3.1 接口类型的转换

1.鸭子类型:指某一物种叫起来像鸭子,走起来像鸭子 那我们就认为它是鸭子。
Go语言对基础类型的类型一致性要求非常严格,但对于接口的类型转换非常灵活

var(
    a io.ReadCloser = (*os.File)(f)    //隐式转换,*os.File满足io.ReaderCloser接口
    b io.Reader = a                    //隐式转换,io.ReaderCloser满足io.reader接口
    c io.Closer = a                    //隐式转换,io.ReaderCloser满足io.Closer接口
    d io.Reader = c.(io.reader)        //显示转转,io.Closer不满足io.Reader接口
)

但是有的时候,接口的转换过于灵活,可能产生无意间的适配,这需要人为限制。通常的做法是定义一个特殊的方法来区分接口,但是这只是“君子协定”,而更严格的做法是给接口定义一个私有方法(此时只有这个类被定义的包中能访问这个接口)但是这个防护也不是绝对安全。
2.Go语言的接口类型是延迟绑定(编译时私有方法是否真的存在并不重要),可以实现类似虚拟函数的多态功能(调用私有类的相关方法)。
常见的用法:内嵌一个(私有)类

type ColoredPoint struct {
    Point                    //内置的(私有)类
    Color color.RGBA
}

3 面向并发的内存模型

Go语言将CSP模型的并发编程内置到了语言中,与Erlang不同的是Go语言的Gotoutine是共享内存的

3.1 Goroutine和系统线程

1.Goroutine会以一个很小的栈空间启动,并且当栈空间不足的时候,会根据需要动态地伸缩栈的大小。
2.Goroutine采用的是半抢占式的协作调度,只有当当前的Goroutine发生阻塞时才会导致调度。

3.2 原子操作

原子操作就是并发编程中“最小且不可并行化”的操作,可以保证共享资源的完整性。在一般情况下,原子操作都是通过“互斥”访问来保证的。
但是互斥锁的方法麻烦而且效率低下,常用标准库sync/atomic的方法操作(同时,atomic.Value原子对象提供了Load()和Store()两种原子方法,用于加载和保存数据,返回任意类型的参数):

import (
    "sync"
    "sync/atomic"
)

var total uint64

func worker(wg * sync.WaitGroup){
    defer wg.Done()
    
    var i uint64
    for i:= 0; i <= 100; i++ {
        atomic.AddUint64(&total, i)        //保证了total的相关操作时原子性质的
    }
}

func main(){
    var wg *sync.WaitGroup
    wg.Add(2)
    
    go worker(&wg)
    go worker(&wg)
    wg.Wait()
}

当然,在性能敏感的地方可以增加一个数字型标志位,通过原子检测标志位的状态降低会互斥锁的使用频率来提高性能。

3.4 顺序一致性内存问题

为了最大化并行,Go语言编译器和处理器在不影响Goroutine内部顺序的前提下可能会对执行语句进行重新排序(CPU也会对一些指令乱序执行)。
一般通过同步原语来给两件事进行明确的排序(同步通道),或者使用sync.Mutex互斥量的Lock()和Unlock()也可以实现。

4 常见的并发模式

Go语言提倡的是使用通道(channel)保证并发的顺序,使用带缓存的通道控制并发数。

4.1 素数筛

//GeneratNatural 生成自然数,并返回通道
func GenerateNatural() chan int {
    ch := make(chan int)
    go func(){
        for i:= 2;; i++{
            ch<-i
        }
    }()
    return ch
}

//通道过滤器,删除能被素数整除的数
func PrimeFilter(in <-chan int, prime int) chan int {
    out := make(chan int)
    go func(){
        for{
            if i := <-in; i%prime != 0 {
                out<-i
            }
        }
    }()
    return out
}

func main(){
    ch := GenerateNatural()
    for i := 0; i < 100; i++{
        prime := <-ch
        fmt.Printf("%v: %v\n", i+1, prime)
        ch = PrimeFilter(ch, prime)    //利用上一个数排除能把ta整除的数,类似于迭代
    }
}

由于每个并发处理的事情太细微,程序的性能并不理想。

4.2 并发的安全退出

通常需要使用select关键字,即当select有多个分支的时候, 会选择一个随机的可用的通道分支, 若没有,则选择default分支, 否则会一直保持阻塞的状态。

select {
case v := <- in:
    fmt.Println(v)                        //获取到了值
case <- time.After(time.Second):        //超时
    return
}

注意,通道的发送操作和接受操作是一一对应的,如果要停止多个Goroutine,那么需要创建同样数量发=的通道,代价太大了。而我们可以close一个通道来实现广播效果,从而所有从此通道的接受操作均会收到一个零值和一个可选失败标志但是Goroutine退出的时候会进行一定的清理工作,需要增加原子操作使main函数管控所有的线程:

func worker (wg *sync.WaitGroup, cannel chan bool){
    defer wg.Done()
    
    for{
        select{
        default:
            fmt.Println("hello")
        case <-cannel:
            return
        }
    }
}

func main(){
    cancel := make(chan bool)
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(&wg, cancel)
    }
    time.Sleep(time.Second)
    close(cancel)
    wg.Wait()
}

4.3 contex包

contex包主要用于简化对于处理单个请求的多个goroutine之间的请求域的数据、超时和退出等操作。(更多操作,见官方文档)

func worker (wg *sync.WaitGroup, ctx contex.Context) error {
    defer wg.Done()
    
    for{
        select{
        default:
            fmt.Println("hello")
        case <-ctx.Done():
            return ctx.Err()
        }
    }
}

func main(){
    ctrx, cancel := contex.WithTimeout(contex.Background(), 10*time.Second)
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(&wg, ctx)
    }
    time.Sleep(time.Second)
    cancel()
    wg.Wait()
}

5 错误和异常

5.1 错误处理策略

在需要进行关闭操作的时候,建议使用defer操作确保在任何特殊情况退出时能够保证关闭操作的执行,同时确保了代码的整洁性。

5.2 获取错误的上下文

若是有这种需求,可用定义自己的包,包含所有的错误类型。

type Error interface {
    Caller() []CallerInfo
    Wraped() []error
    Code()   []int
    //作为接口类型的扩展,增加调用栈信息,支持错误的多级嵌套包装,支持错误码格式
    error
    private()
}

type CallerInfo struct {
    FuncName string
    FileName string
    FileLine int
}

//定义相应的辅助函数
func New (msg string) error                    //创建新的错误类型
func NewWithCode(code int, msg string) error//适用于http的创建
func Wrap(err error, msg string)            //进行一层是错误包装,保留底层信息
func WrapWithCode(code int, err error, msg string)
func formJson(json string)(Error, error)    //传输使用
func ToJson(err error) string

5.3 错误的错误返回

在错误的返回时,没有错误最好直接返回nil

5.3.1 剖析异常

recover()必须要和有异常的栈帧只隔一个栈帧时才能捕获异常。说人话就是要在defer(的一层)函数中调用才可用捕获异常

func main(){
    defer func (){recover()}()
    //其他操作
}
0

评论

博主关闭了所有页面的评论