Ezreal 书架 Ezreal 书架
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
  • 逃逸分析

  • 延迟语句

  • 数据容器

  • 通道

  • 接口

    • Go 接口与 C++接口有何异同
    • Go 语言与“鸭子类型”的关系
    • iface 和 eface 的区别是什么
    • 值接收者和指针接收者的区别
    • 如何用 interface 实现多态
    • 接口的动态类型和动态值是什么
    • 接口转换的原理是什么
    • 类型转换和断言的区别是什么
      • 类型转换
      • 断言
    • 如何让编译器自动检测类型是否实现了接口
  • unsafe

  • context

  • Go程序员面试笔试宝典
  • 接口
ezreal_rao
2023-06-01
目录

类型转换和断言的区别是什么

Go 语言中不允许隐式类型转换,也就是说符号 “=” 两边,不允许出现类型不相同的变量。 类型转换、类型断言本质都是把一个类型转换成另外一个类型。不同之处在于类型断言是对接口变量进行的操作。

# 类型转换

对于类型转换而言,转换前后的两个类型要相互兼容才行。类型转换的语法为:

<结果类型> := <目标类型> (<表达式>)
1

来看一下例子:

package main

import "fmt"

func main() {
    var i int = 9
    var f float64
    f = float64(i)
    fmt.Printf("%T, %v\n", f, f)
    f = 10.8
    a := int(f)
    fmt.Printf("%T, %v\n", a, a)
    // s := []int(i)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面的代码里,定义了一个 int 型和 float64 型的变量,在它们之间相互转换,结果是成功 的:int 型和 float64 是相互兼容的。

如果把最后一行代码的注释去掉并运行,编译器会报告类型不兼容的错误:

cannot convert i (type int) to type []int
1

# 断言

因为空接口 interface {} 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface {},那么在函数中,需要对形参进行断言,从而得到它的真实类型。

断言的语法为:

<目标类型的值>,<布尔参数> := <表达式>.(目标类型) // 安全类型断言
<目标类型的值> := <表达式>.(目标类型) //非安全类型断言
1
2

类型转换和类型断言有些相似,不同之处,在于类型断言是对接口进行的操作。来看一个简短的例子:

package main

import "fmt"

type Student struct {
    Name string
    Age int
}

func main() {
    var i interface{} = new(Student)
    s := i.(Student)
    fmt.Println(s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

运行一下:

panic: interface conversion: interface {} is *main.Student, not main.Student
1

直接 panic 了,这是因为 i 是 *Student 类型,并非 Student 类型,所以断言失败。如果是生产环境的代码,可以采用 “安全断言” 的语法:

func main() {
    var i interface{} = new(Student)
    s, ok := i.(Student)
    if ok {
        fmt.Println(s)
    }
}
1
2
3
4
5
6
7

这样,即使断言失败也不会 panic。

断言其实还有另一种形式,就是用于 switch 语句判断接口的类型,每一个 case 会被顺序地考虑。当命中一个 case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为可能会有多个 case 匹配的情况。

代码示例如下:

func main() {
    //var i interface{} = new(Student)
    //var i interface{} = (*Student)(nil)
    var i interface{}
    fmt.Printf("%p %v\n", &i, i)
    judge(i)
}
func judge(v interface{}) {
    fmt.Printf("%p %v\n", &v, v)
    switch v := v.(type) {
    case nil:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("nil type[%T] %v\n", v, v)
    case Student:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("Student type[%T] %v\n", v, v)
    case *Student:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("*Student type[%T] %v\n", v, v)
    default:
        fmt.Printf("%p %v\n", &v, v)
        fmt.Printf("unknow\n")
    }
}

type Student struct {
    Name string
    Age int
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

在 main 函数里有三行不同的声明,按顺序每次运行一行,注释另外两行,得到三组运行结果:

// --- var i interface{} = new(Student)
0xc4200701b0 [Name: ], [Age: 0]
0xc4200701d0 [Name: ], [Age: 0]
0xc420080020 [Name: ], [Age: 0]
*Student type[*main.Student] [Name: ], [Age: 0]

// --- var i interface{} = (*Student)(nil)
0xc42000e1d0 <nil>
0xc42000e1f0 <nil>
0xc42000c030 <nil>
*Student type[*main.Student] <nil>

// --- var i interface{}
0xc42000e1d0 <nil>
0xc42000e1e0 <nil>
0xc42000e1f0 <nil>
nil type[<nil>] <nil>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

对于第一行语句:

var i interface{} = new(Student)
1

因为 i 是 *Student 类型,匹配上第三个 case。从打印的 3 个地址来看,这 3 处的变量实际上都是不一样的。在 main 函数里有一个局部变量 i,调用函数时,实际上是复制了一份参数,因此函数里又有一个变量 v,它是 i 的复制。断言之后,又生成了一份新的复制。所以最终打印的三个变量的地址都不一样。

对于第二行语句:

var i interface{} = (*Student)(nil)
1

这里想说明的其实是 i 在这里的动态类型是 *Student , 数据为 nil,它的类型并不是 nil,它与 nil 做比较的时候,得到的结果也是 false。

最后一行语句:

var i interface{}
1

这回 i 才是 nil 类型。

最后需要提醒一点的是,代码 v.(type) 中,v 只能是一个接口类型,如果是其他类型,例如 int 型,会导致编译不通过。

函数 fmt.Println 的参数是 interface。对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 String () 方法,如果 实现了,则直接打印输出 String () 方法的结果;否则,会通过反射来遍历对象的成员进行打印。 再来看一个简短的例子:

package main

import "fmt"

type Student struct {
    Name string
    Age int
}

func main() {
    var s = Student{
        Name: "qcrao",
        Age: 18,
    }
    fmt.Println(s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

因为 Student 结构体没有实现 String () 方法,所以 fmt.Println 会利用反射挨个打印成员变量:

{qcrao 18}
1

如果增加一个 String () 方法的实现:

func (s Student) String() string {
    return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}
1
2
3

打印结果如下:

[Name: qcrao], [Age: 18]
1

可以看到,会按照自定义的方法来打印。 针对上面的例子,如果改一下 String 方法的接收者类型:

func (s *Student) String() string {
    return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}
1
2
3

注意看两个函数的接收者类型不同,现在 Student 结构体只有一个接收者类型为指针类型的 String () 函数,打印结果如下:

{qcrao 18}
1

为什么会这样?前面讲过:类型 T 只有接受者是 T 的方法;而类型 *T 拥有接受者是 T 和 *T 的方法。语法上 T 能直接调 *T 的方法仅仅是 通过 Go 语言的语法糖。

所以,当 Student 结构体定义了接受者类型是值类型的 String () 方法时,通过

fmt.Println(s)
fmt.Println(&s)
1
2

均可以按照自定义的格式来打印。 如果 Student 结构体定义了接受者类型是指针类型的 String () 方法时,只有通过

fmt.Println(&s)
1

才能按照自定义的格式打印。 有一个值得注意的问题是需要防止有关自定义 String () 方法时无限递归,例如:

type Student struct {
    Name string
    Age int
}

func (s Student) String() string {
    return fmt.Sprintf("%v", s)
}
func main() {
    s := Student{
        Name: "qcrao",
        Age: 19,
    }
    fmt.Printf("%v", s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

直接运行,最后会导致栈溢出:

fatal error: stack overflow
1

如果类型实现了 String () 方法,格式化输出时就会自动调用 String () 方法。上面这段代码是在该类型的 String () 方法内使用格式化输出,导致递归调用 String () 方法,引发栈溢出。

改进的办法是修改 String () 方法的实现:

func (s Student) String() string {
    return fmt.Sprintf("%v", s.Name+ " " + strconv.Itoa(s.Age))
}
1
2
3

Go 语言的 switch 用法多样, 非常灵活。 那么 switch 有哪几种用法?和其他语言, 如 C/C++、Java 等不同的是,Go 的 switch 语句从上到下进行匹配,仅执行第一个匹配成功的分支。 因此 Go 不用在每个分支里都增加 break 语句。另外一个不同点在于,Go switch 语句的 case 值不需要是常量,也不必是整数。

用法一:比较单个值和多个值。

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.\n", os)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

直接在 switch 语句内声明 os 变量,使得 os 的作用范围仅在 switch 语句内。

用法二:每个分支单独设置比较条件。

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}
1
2
3
4
5
6
7
8
9
10
11

直接在 case 语句中判断表达式的真假,并且只会执行第一个满足条件的 case。

用法三:使用 fallthrough 关键字。

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    fallthrough
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

分支中的 fallthrough 关键字,表示执行下一个分支。如果当前时间的小时数既小于 12,也小于 17,那么程序最后就会打印出:

Good morning!
Good afternoon.
1
2

为了使 switch 语句看起来更简洁,可以将多个 case 用逗号分隔,合并成一个分支:

func main() {
    fmt.Println("When's Saturday?")
    today := time.Now().Weekday()
    switch time.Saturday {
    case today + 0, today + 1:
        fmt.Println("coming...")
    case today + 2:
        fmt.Println("In two days.")
    default:
        fmt.Println("Too far away.")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

将 today+0 和 today+1 放在一个 case 分支里,如果今天是周五或周六都会打印出:

coming...
1
#golang#interface
上次更新: 7/12/2024, 2:37:05 PM
接口转换的原理是什么
如何让编译器自动检测类型是否实现了接口

← 接口转换的原理是什么 如何让编译器自动检测类型是否实现了接口→

最近更新
01
为什么我的MySQL会抖一下
07-15
02
HTTP 性能优化面面观
07-12
03
WebSocket:沙盒里的 TCP
07-12
更多文章>
Theme by Vdoing | Copyright © 2022-2024 Ezreal Rao | CC BY-NC-SA 4.0
豫ICP备2023001810号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式