Go 基础 - 记录一些 Go 的特殊语法(三)

713 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

2022-07-25-09-23-06-image.png

前言

Go 基础 - 记录一些 Go 的特殊语法(二)

本系列文章旨在分享,记录 Go 中的特殊语法,帮助 Gopher 提升对 Go 语法细节的理解和认识。

Go 的特殊语法

分配

上一篇结尾讲到了数组与切片,那么本篇开始就先来讲讲分配。

Go 中提供了两种用来分配的内建函数,分别是 newmake,它们做的事情和适用的类型都不同,可能会引起混淆,下面来详细说说。

new

new 在分配内存时,不会初始化内存,只会将内存置零。 也就是说,new(T) 会返回一个指针, 该指针指向新分配的,类型为 T 的零值。

例如

package main
func main() {
    var i = new(int)
    println(i, *i)
    var j = new(float64)
    println(j, *j)
    var k = new([]int)
    println(k, *k)
}

result:
0xc00005bf48 0
0xc00005bf40 +0.000000e+000
0xc00005bf58 [0/0]0x0

主要看最后一行,对切片分配内存后,返回的是一个0长度0容量指向地址0的切片的指针,这样的切片肯定是没有办法使用的,因为地址0不可访问(实际上指向地址0的指针就是nil),所以可以看出来此时切片是没有底层数组的。

make

make(T, args) 只用于创建切片、映射和信道,并返回类型为 T(而非 *T)的一个已初始化的值,这是由于引用数据类型在使用前必须初始化。

下面的示例展示了 newmake 的不同:

package main
func main() {
    var k = new([]int)
    println(k, *k)
    *k = make([]int, 100)
    println(k, *k)
    v := make([]int, 10)
    println(&v, v)
}

result:
0xc00005bf58 [0/0]0x0
0xc00005bf58 [100/100]0xc000072000
0xc00005bf40 [10/10]0xc00005bee8

类型

类型转换

当我们声明了一个新类型 type Sequence []int 后,想要让 Sequence 能够使用 sort 包中的 Sort 方法。最朴素的想法是用 Sequence 实现 sort.Interface 接口。但实际上,sort 包中已经为 []int 写好了对接口的实现(升序)叫 IntSlice 类型,我们可以直接对 Sequence 进行类型转换,然后使用 Sort 方法。

type Sequence []int
func (s Sequence) String() string {
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}
func main() {
    s := make(Sequence, 0)
    s = append(s, 2, 5, 1, 4, 3, 6)
    fmt.Println(s)
}

同时,类型转换也是防止在 Sprint Sprintf 中产生递归调用的解决办法之一。

类型选择

类型选择是类型转换的一种形式:它接受一个接口,在 switch 中根据其判断选择对应的 case ,并将其转换为该种类型。

以下代码通过类型选择将值转换为字符串:如果它已经是字符串,直接打印字符串值;若它有 String 方法,则打印调用该方法所得的结果。

package main
import (
    "fmt"
    "sort"
)
type Stringer interface {
    String() string
}
type Sequence []int
func (s Sequence) String() string {
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}
func main() {
    var value interface{}
    // value = "sss"
    v := make(Sequence, 0)
    value = append(v, 2, 1, 5, 3, 0)
    // value = 2
    switch str := value.(type) {
    case string:
        println(str)
    case Stringer:
        println(str.String())
    default:
        println("error")
    }
}

类型断言

类型断言接受一个接口值,并从中提取指定的明确类型的值。它需要一个明确的类型,而非 type 关键字: value.(typeName),其结果则是拥有静态类型 typeName 的新值。该类型必须为该接口所拥有的具体类型,或者该值可转换成的第二种接口类型。要提取我们知道在该值中的字符串,可以这样:

str := value.(string)

但如果它所转换的值中不包含字符串,该语句就会触发一个恐慌。为避免这种情况,需使用 , ok 惯用语句来测试它的值是否为字符串:

if str, ok := value.(string), ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

若类型断言失败,str 将继续存在且为字符串类型,但它将拥有零值,即空字符串。

空白标识符

_ 可以被赋予或声明为任何类型的值,并把值舍弃,用在需要变量但不需要实际值的地方作为占位符。这一特性使它具有很多神奇的用处。

骗过编译器对 unused 的检查

程序开发过程中,可能经常会产生未使用的导入和变量,我们可以用 _ 来使它们能够通过编译。

package main
import (
    "fmt"
    "log"
    "os"
)
var _ = fmt.Printf
func main() {
    fs, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    _ = fs
}

为副作用而导入

有时导入某个包只是为了其副作用, 而没有任何明确的使用。例如,在 net/http/pprof 包的 init 函数中记录了HTTP处理程序的调试信息。它有个可导出的API, 但大部分客户端只需要注册的 handler 信息。所以当只为了副作用来导入包时,可以将包重命名为空白标识符 import _ net/http/pprof"

接口检查

之前在《HttpRouter 源码分析》的文章中讲过,它里面为了检查 Router 是符合 http.Handler 接口的实现,写了这样一句 var _ http.Handler = New() 这里的 New() 返回的是 *Router

总结

本篇文章介绍了分配函数 newmake 的不同(一个是置零,一个是初始化),类型选择、转换和断言,以及空白标识符 _ 的一些神奇的使用方法。

最后,如果本篇文章对你有所帮助,希望你可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿