golang 面试常问(简短变量声明、字符串、切片、defer)

目录

简短变量声明 :=

基本概念

掉坑日常

字符串 string

基本概念

切片 slice

基本概念

nil slice 和 empty slice

nil slice

empty slice

浅拷贝和深拷贝

切片扩容

掉坑日常

defer

基本概念

掉坑日常


简短变量声明 :=

基本概念

1、:= 是 变量声明语句,不是变量赋值操作;

2、左边的变量至少要有一个是没有声明过的,即至少要声明一个新变量;

3、承接第二点,左边的变量可能不是全部刚刚声明的,有的变量可能已经在 同级词法域中 声明过,此时简短变量声明语句对于这种变量就变成了赋值操作了。

// 声明两个变量 in 和 err
in, err := os.Open(infile)
​
// 声明一个变量 out
out, err := os.Create(outfile)

掉坑日常

var cwd string
func init() {
    cwd, err := os.Getwd() 
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd)
}

上面代码你能看出来问题吗?

首先,init() 函数外部声明变量 cwd,函数内部包含一个简短变量声明语句,这其实很符合我们的写法,因为这里我们想要声明一个新的 err 变量来判断错误,如果没有错误,我们期望会给外部声明的 cwd 这个变量赋值。

然而,其实函数内和函数外的两个 cwd 变量 不属于同级词法域,因此,内部的 cwd 其实是一个新声明的局部变量,和外部的 cwd 没有任何关系。

修改如下:

var cwd string
func init() {
    // 将 err 单独声明
    var err error 
    //  cwd 和 err 直接使用多变量赋值,不再使用 :=
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

字符串 string

基本概念

1、一个字符串是一个 不可改变 的 字节 序列, 函数 len(s) 返回的是 字节数,s[i] 返回的是 第 i 个字节,这里强调的都是 字节,区别于字符,go 采用 UTF-8 编码,UTF-8 编码是针对 Unicode 的一种可变长度字节编码, 一个字符占1-4 个字节;

字符集:我们熟知的 ASCII 码字符集只有 128 个字符,无法表示中文字符,GB18030 是汉字字符编码方案的国家标准,它囊括了基本所有的汉字,但是它又没法表示其他国家的字符,因此,国际组织发明了 Unicode 字符集,它收集了世界上所有的字符,为每个字符都分配一个唯一的码点。 编码方式:字符集内所有字符都有一个对应的码点,比如 49 对应 字符 ‘0’, 65 对应字符 ‘A’,19990 对应字符 ‘世’ ...,要怎么表示所有的字符呢?直观的方式是用最大的那一个字符长度所需的字节数来编码所有字符,比如我们每个字符都用 4 个字节来编码,这样肯定可以保证一一对应;但是造成了很大的空间浪费,比如字符 ‘0’ 的编码是 49,本来一个字节足够表示,这样就白白浪费了 3 个字节;我们看看 UTF-8 怎么解决这个问题; UTF-8 编码:UTF-8 使用 1到 4 个字节来表示每个字符, ASCII 部分字符只使用1个字节, 常用字符部分使用 2 或 3 个字节表示。 每个符号编码后第一个字节的高端 bit 位表示编码总共有多少个字节。 如果第一个字节的高端bit为 0, 则表示对应 7 bit 的 ASCII 字符, ASCII字符每个字符依然是一个字节, 和传统的ASCII编码兼容。 如果第一个字节的高端 bit 是110, 则说明需要2个字节; 后续每个高端 bit 都以 10 开头。 更大的以此类推:

0xxxxxxx                                                 0-127

110xxxxx 10xxxxxx                                 128-2047

1110xxxx 10xxxxxx 10xxxxxx                  2048-65535

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff

2、在 go 中,字节用 byte 表示, 字符用 rune 表示;

3、区分以下两种字符串的遍历,一个是按字节 byte 遍历,一个是按字符 rune 遍历:

func main() {
    str := "Hello,世界"
    // 方式一,按字节遍历
    for i := 0; i < len(str); i++ {
        ch := str[i]
        fmt.Println(ch)
    }
    // 方式二,按字符遍历
    for _, ch1 := range str {
        fmt.Println(ch1)
    }
}

切片 slice

基本概念

1、一个 slice 代表一个变长序列,属于 引用类型,底层引用一个数组对象,一个 slice 由三部分组成:

  • 指针:指向第一个元素对应的底层数组元素的地址;

  • 长度:slice 中元素的个数,长度小于等于容量;

  • 容量:slice 开始位置到底层数组的结尾位置。

2、一个切片只能和 nil 比较,切片之间不可以比较;

3、创建切片时,如果已知容量大小,建议提前分配好容量,避免追加过程中进行扩容降低性能;

4、切片是非线程安全的。

nil slice 和 empty slice

nil slice

var s []int 声明了一个切片变量 s, go 初始化机制会将变量 s 初始化为 nil,它对应的长度和容量都是 0,没有底层数组,此时 s == nil;

empty slice

var s = []int{} 或 var slice = make([]int, 0), 前面语句生成了一个空切片 s,它的长度和容量都等于 0,指针指向一个固定地址, 此时 s != nil。

浅拷贝和深拷贝

  • 浅拷贝:形如 slice1 = slice2 、 slice1 = slice2[i:j] 等,只拷贝切片结构体,也就是说拷贝后两个切片指向同一个底层数据,长度和容量都相等;
  • 深拷贝: copy(desc, src),两个切片指向不同的底层数组,不再有任何关联。

切片扩容

需要注意的是,不同的版本扩容机制存在不同,下面给的是 go1.18 的版本:

//1.18
newcap := old.cap
doublecap := newcap + newcap
// 如果新申请容量 大于 两倍原有容量,那么扩容后容量大小 等于 新申请容量;
if cap > doublecap {
  newcap = cap
} else {
  const threshold = 256
  // 当原 slice 容量 < 256 的时候,新 slice 容量变成原来的 2 倍;
  if old.cap < threshold {
    newcap = doublecap
  } else {
    // 当原 slice 容量 > 256,每次容量增加(旧容量+3*threshold)/4
    for 0 < newcap && newcap < cap {
      newcap += (newcap + 3*threshold) / 4
    }
    ... 
}

扩容大小根据原有容易和要扩容的容量分为三种情况:

  • 如果新申请容量 大于 两倍原有容量,那么扩容后容量大小 等于 新申请容量;

  • 如果原 slice 容量 < 256,新 slice 容量变成原来的 2 倍;

  • 如果原 slice 容量 > 256,每次容量增加(旧容量+3*threshold)/4。

掉坑日常

1、 多个 slice 可以 共享底层数组,因此,某一个 slice 的更新可能会影响到其他的 slice;

func main() {
    var res [][]int
    s1 := []int{1,2,3}
    res = append(res, s1)
    s1[1] = 4
    
    fmt.Println(res)
}

以上 res 输出的结果你知道吗?

答案是 [[1 4 3]]。这个例子比较简单,你可能一眼能看出来,但是如果经过将 s1 作为参数,传递到另外一个函数添加到 res 里的时候,就不那么明显了。别问我为啥知道,第一次写回溯算法的时候差点去撞墙。

修改方法是保证添加到 res 里的切片和 s1 切片不再引用同一个底层数组,这样以后 s1 的修改就不会影响到 res, 具体代码我们待会看完下一个坑后一起给出。

2、浅拷贝引发的内存泄漏

var s1 []int
​
func test(s2 []int) []int {
    s1 = s2[:1]
    return s1
}
​
func main() {
    s2 := []int{1,2,3,.....10000}
    test(s2)
}

以上代码你看出来问题了吗?

s1 = s2[:1] 使得 s1 和 s2 引用了同一个底层数组,尽管以后对 s1 的使用可能就是那一个元素,但是底层数组却因为 s1 的引用不能释放。

解决方法就是想办法给切片 s1 重新生成一个底层数组,使得切片 s2 引用的底层数组可以得到释放。

看到没,其实和上一个坑是一样的原因,都是因为 不同的切片引用到了同一个底层数组 导致的问题,只是问题造成的影响有所不同,解决方法都是想办法让不同的切片引用各自的底层数组就好了。

可行的修改方法:

func main() {
    var s1 []int
    s2 := []int{1,2,3}
    
    // 方法一,利用 append 扩容的机制生成新的底层数组
    s1 = append(s1, s2[:1]...)
    
    // 方法二,通过深拷贝为目的切片生成新的底层数组
    s3 := make([]int, 1)
    copy(s3, s2[:1])
    
    fmt.Println(&s1[0], &s2[0], &s3[0]) // 0xc00001a0b0 0xc00000c3e0 0xc00001a098
}

defer

基本概念

1、关键字 defer, 加在函数或方法前面,即可完成对函数或方法的延迟调用;

2、当 defer 语句执行时, 函数和参数表达式会直接进行计算, 但要等到 包含该 defer 语句的函数 执行完毕时, defer 后的函数才会被执行;

3、存在多个defer 调用时,执行顺序与声明顺序相反;

掉坑日常

1、在循环中使用 defer

for _, filename := range filenames {
    f, err := os.Open(filename)
    if err != nil {
        return err
    } 
    defer f.Close()
    ...
}

以上代码有什么问题?

defer 语句本身就最适合用于这种需要资源释放的场景,但是以上写法忽略了一点, defer 后面的函数要等到包含该语句的函数执行完毕后才会执行,以上如果 filenames 很大,通过 defer 调用的关闭文件函数一直得不到执行,可能最终会导致文件描述符的耗尽。

像这种场景可以考虑不使用 defer 语句,或者将循环体放置在一个匿名函数中加以解决;

以下通过添加匿名函数加以修改:

for _, filename := range filenames {
    func(){
        f, err := os.Open(filename)
        if err != nil {
            return err
        } 
        defer f.Close()
        ...
    }() 
}

此时,defer 语句位于匿名函数内,每遍历一个文件时都是在匿名函数中进行,进而可以及时的释放。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值