0%

Go语言基础

视频教程:https://www.bilibili.com/video/BV1gf4y1r79E

Golang 下载与安装

Golang 安装包

Windows 安装教程:https://juejin.cn/post/6982916943183347742

在下方两个网站中任选其一,下载对应操作系统的 Golang 安装包: > 谷歌官方网站:https://go.dev/dl/ > 国内镜像网站:https://studygolang.com/dl

安装包下载之后直接安装,如有修改安装路径的需求外,其余一路 next 即可。安装完毕之后,在命令行窗口输入 go version查看 Go 的版本,如果显示版本号,则表示安装成功

Golang 环境变量

Golang 需要配置三个环境变量,分别是: - GOROOT:Golang 的安装目录 - PATH:Golang 的安装目录下的 bin 目录 - GOPATH:Golang 的项目目录

如果使用的是 msi 安装包,一般都会在安装的时候自动配置GOROOTPATH,只要go version能够查看版本信息,就说明 Golang 配置成功,这两个环境变量无需再配置了;如果使用的是 zip 解压缩的方式,那么就需要手动配置这些环境变量

注意:本人 Go 版本为 1.21,文章编辑于 2023年12月。新版本的 Go 会在用户变量中自动创建这三个环境变量,其中GOROOTPATH保持不变即可,只需要修改GOPATH

接下来配置GOPATH环境变量,我们需要提前新建一个我们想要开发 Go 项目的文件夹,然后在系统变量中修改名为GOPATH的环境变量,值为该文件夹路径

在该文件夹中新建三个文件夹,分别是bin(二进制可执行文件)、pkg(用于存放依赖包)、src(用于存放我们自己写的代码)

使用 VSCode 开发 Go 项目

我们接下来使用 VSCode 开发 Go 项目,如果想使用 JetBrains 专门用于Go语言程序编写的 Goland 进行开发也是可以的

但不管使用什么 IDE 开发 Go 项目,建议将默认的GOPROXY代理地址修改为国内镜像地址,否则在下载外部依赖包时容易出现超时问题。在命令行窗口中输入该命令进行修改:

1
go env -w GOPROXY=https://goproxy.cn,direct

最后使用go env命令查看各项环境配置参数

  1. 使用 VSCode 开发 Go 项目需要下载相关插件,首先下载 Go 插件

  1. 先新建一个 main.go文件,右下角会自动弹出提示,让你下载其他依赖,点击"安装全部"

  1. 在“输出”中查看安装情况,如下图表示全部安装成功

Golang 语言基础

初窥 Golang 语法:Hello World

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main // 当前程序的包名

// 导单个包
// import "fmt"
// import "time"

// 导多个包
import (
"fmt"
"time"
)

// main函数
func main() {
time.Sleep(2 * time.Second)

fmt.Println("Hello, World!")
}

注意: 1. Go 加不加分号都可以,建议不加 2. 函数的{必须和函数名在同一行,否则编译错误

声明变量与常量

  1. 声明变量
    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // 四种变量的声明方式
    package main

    import "fmt"

    // 方式一二三支持全局变量,而方式四不支持
    var gA int
    var gB = 100
    var gC = "Hello, World!"
    // gD := 100

    func main() {
    // 方式一:声明一个变量,默认值为0
    var a int
    fmt.Println("a = ", a)

    // 方式二:声明一个变量,并赋初始值
    var b int = 100
    fmt.Println("b = ", b)

    // 方式三:赋初始值时,可以省略数据类型,根据值自动匹配当前变量的数据类型
    var c = "Hello, World!"
    fmt.Printf("c = %s, c 的类型是 %T\n", c, c)

    // 方式四:(常见)省去var关键字,直接自动匹配数据类型
    d := 100
    fmt.Printf("d = %d, d 的类型是 %T\n", d, d)

    fmt.Println("gA = ", gA, "gB = ", gB, "gC = ", gC)

    // 声明多个变量
    var ee, ff int = 100, 200
    fmt.Println("ee = ", ee, ", ff = ", ff)
    var gg, hh = 100, "abc"
    fmt.Println("gg = ", gg, ", hh = ", hh)

    // 多行的多变量声明
    var (
    ii int = 100
    jj bool = true
    )
    fmt.Println("ii = ", ii, ", jj = ", jj)
    }
  2. 声明常量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package main

    import "fmt"

    // 定义枚举类型
    const (
    // BEIJING = 0
    // SHANGHAI = 1
    // GUANGZHOU = 2

    // 在const()添加关键字 iota,每行的iota都会自增1,第一行iota默认为0
    BEIJING = iota // iota = 0
    SHANGHAI // iota = 1
    GUANGZHOU // iota = 2
    )

    func main() {
    // 常量(只读属性)
    const length int = 10
    fmt.Println("length = ", length)
    // length = 100 // 常量不允许被修改

    fmt.Println("BEIJING = ", BEIJING, "SHANGHAI = ", SHANGHAI, "GUANGZHOU =", GUANGZHOU)
    }

小技巧:VSCode 代码片段

我们可以类似 Java 中通过sout快捷输入System.out.println一样,为 Go 语言设置自己的代码片段 1. 创建 Go 的代码片段,按如下步骤弹出窗口后,输入“go”

  1. go.json中配置main方法和fmt.println()的代码片段,比如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "func main": {
    "prefix": "fm",
    "body": [
    "package main",
    "",
    "func main() {",
    "\t$0",
    "}"
    ],
    "description": "定义 main 方法"
    },
    "fmt Println": {
    "prefix": "fp",
    "body": [
    "fmt.Println($0)"
    ],
    "description": "输出语句"
    }
    }
    这样,只要输入fm就可以自动编写main方法的一些代码,输入fp就可以自动编写fmt.Println()

函数的多返回值

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
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import "fmt"

// 定义函数的形参、单个返回值
func fool(a string, b int) int {
fmt.Println("a = ", a)
fmt.Println("b = ", b)

c := 100
return c
}

// 定义函数的多个返回值(匿名)
func fool2() (int, int) {
return 666, 777
}

// 定义函数的多个返回值(指定名称的)
func fool3() (r3 int, r4 int) {
r3 = 100
r4 = 200
return
}

// 定义函数的多个返回值(指定名称的,数据类型如果一致可以写一起)
func fool4() (r5, r6 int) {
r5 = 100
r6 = 200
return
}

func main() {
c := fool("abc", 100)
fmt.Println("c = ", c)

retVal1, retVal2 := fool2()
fmt.Println("retVal1 = ", retVal1, " retVal2 = ", retVal2)

retVal3, retVal4 := fool3()
fmt.Println("retVal3 = ", retVal3, " retVal4 = ", retVal4)
}

如果有func fool(a int, b int) (c int, d int),不但ab属于形参,cd也属于形参,同样只能作为局部变量在函数体里使用,但ab的默认值由调用者传参决定,而cd的默认值是0

导包

建立两个目录lib1lib2,并在两个目录中分别新建两个 go 文件lib1.golib2.golib1.go这么写,lib2.go同理:

1
2
3
4
5
6
7
8
9
10
11
12
package lib1

import "fmt"

// lib1提供的API
func Lib1Test() {
fmt.Println("lib1开始了...")
}

func init() {
fmt.Println("lib1的init方法执行了...")
}

然后新建main.go,通过导入上面两个包,调用其提供的API

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"go_study/05_init/lib1"
"go_study/05_init/lib2"
)

func main() {
lib1.Lib1Test()
lib2.Lib2Test()
}
注意: 1. 新版本可能需要更改 go 环境的配置go env -w GO111MODULE=off才可以导包成功,后续还需要改回来 2. 导包路径是%GOPATH%/项目目录名/子目录/.../程序名 3. 对外提供的函数名首字母必须大写,否则只能该函数只能在内部使用

运行结果如下所示:

1
2
3
4
lib1的init方法执行了...
lib2的init方法执行了...
lib1开始了...
lib2开始了...
因此,我们可以知道init方法最早开始执行,并且其他方法的执行顺序如下图所示:

另外,导包还有三个注意点: 1. import _ "lib1":给lib1起别名(匿名),表示无法使用lib1的方法,但是会执行lib1init方法 2. import myLib "lib1":给lib1起别名,表示可以使用myLib.Lib1Test()调用lib1的方法 3. import . "lib1":将lib1包中的所有方法导入到当前程序中,即lib1的方法可以直接使用,比如Lib1Test(),而无需lib1.Lib1Test()。但是当导入多个包中有重名的方法会发生歧义,所以建议避免重名

defer关键字

defer关键字用于修饰被调用的函数,使该函数在调用者函数生命周期结束后再执行

比如下方这个代码示例,则展示了defer的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func deferFunc(i int) int {
fmt.Println("传参后的 i =", i)

defer func() {
i += 5
fmt.Println("defer 函数被执行后的 i =", i)
}()

return i
}

func main() {
i := 5
fmt.Println("初始化的 i =", i)
fmt.Println("return 回来的 i =", deferFunc(i))
}
该程序的执行结果是:
1
2
3
4
初始化的 i =  5
传参后的 i = 5
defer 函数被执行后的 i = 10
return 回来的 i = 5
可以看到被defer关键字修饰的函数会比return更晚执行,所以在本例中主函数得到的结果依然是初始化时候的5

另外,defer还有个特点,使用它修饰多个函数时,这些函数的执行顺序遵循LIFO(先进后出)的栈顺序,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func a() {
fmt.Println("a方法执行了...")
}

func b() {
fmt.Println("b方法执行了...")
}

func c() {
fmt.Println("c方法执行了...")
}

func main() {
defer a()
defer b()
defer c()
}
该程序的执行结果是:
1
2
3
c方法执行了...
b方法执行了...
a方法执行了...
以图片的形式可以更直观的感受栈的特点:

数组

固定数组

下方代码展示了如何声明固定长度的数组,以及如何循环遍历数组

其中我们通过range关键字指定某个数组,返回当前元素的下标与值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
// 固定长度的数组
var myArray1 [10]int

for i := 0; i < len(myArray1); i++ {
fmt.Println(myArray1[i])
}

myArray2 := [6]int{1, 2, 3, 4}

for index, value := range myArray2 {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}

//查看数组的数据类型
fmt.Printf("myArray1 的数据类型是 %T\n", myArray1) // [10]int
fmt.Printf("myArray2 的数据类型是 %T\n", myArray2) // [6]int
}
另外我们发现一旦声明了一个整型的固定数组,那么该数组的数据类型是[长度]int,所以当我们使用固定长度的数组进行传参时,是严格匹配数组类型的,即某个函数定义的形参类型是[10]int,那么我们就无法传入[6]int的数组

切片 slice

下方代码展示了如何声明动态数组:

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
package main

import "fmt"

func printArray(arr []int) {
//引用传递
// _ 表示匿名变量
for _, value := range arr {
fmt.Println("value = ", value)
}

arr[0] = 100 // 修改 myArray 的值
}

func main() {
myArray := []int{1, 2, 3, 4, 5} //动态数组,切片slice

// 打印 myArray 的数据类型
fmt.Printf("myArray 的数据类型是 %T\n", myArray) // []int

printArray(myArray)

fmt.Println("----- 修改后 -----")
for _, value := range myArray {
fmt.Println("value = ", value)
}
}
整型动态数组的数据类型是[]int,因此在传参时,我们也只需要将形参的数据类型定义为[]int

而且动态数组在传参时是引用传递,因此可以修改到原数组的元素。

其中for _, value := range arr,由于range固定返回下标和元素值,如果我们不想使用下标,可以使用匿名变量_代替

声明 slice

下方代码展示了两种方式声明slice

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
package main

import "fmt"

func main() {
// 方式一:声明slice,并且初始化值
slice1 := []int{1, 2, 3, 4, 5}

fmt.Printf("len(slice1) = %d, slice1 = %d\n", len(slice1), slice1)

// 方式二:声明slice,并且分配空间,默认初始化值为0
slice2 := make([]int, 4)
slice2[0] = 1

fmt.Printf("len(slice2) = %d, slice2 = %d\n", len(slice2), slice2)

// 定义一个空切片,即没有分配空间
var slice3 []int

fmt.Printf("len(slice3) = %d, slice3 = %d\n", len(slice3), slice3)

if slice3 == nil {
fmt.Println("slice3 is nil")
} else {
fmt.Println("slice3 is not nil")
}
}
在有已知初始化值的时候,我们可以使用方式一;如果没有初始化值,我们可以通过make方法先为slice分配空间,其默认初始化值为0;

另外我们也可以定义空切片,表示没有空间被分配的切片

slice 的追加与截取

下方代码展示了 slice的另一个属性:容量,以及如何追加元素,并且当超过容量时如何扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
var numbers = make([]int, 3, 5)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers) // 3 5 [0 0 0]

// 给 numbers 追加一个元素1
numbers = append(numbers, 1)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers) // 4 5 [0 0 0 1]

// 给 numbers 追加两个元素2 3
numbers = append(numbers, 2, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers) // 6 10 [0 0 0 1 2 3]
}
上述的make方法中有三个参数,第一个表示切片的数据类型、第二个表示切片的长度、第三个表示切片的容量,如果没有指定容量,那么也会默认使容量等于长度

如果把切片比作酒店,那么容量就是酒店当前所有的房间数,长度就是已经有客人的房间,当添加元素时,长度变长,容量不变,直到长度超过容量,那么就需要扩容了。如何扩容?就是将当前容量翻倍。

比如当前容量是5,当长度超过容量时,容量翻倍为10。如果再次超过,那么翻倍为20,以此类推

切片的截取规则和 Python 类似,下方代码展示了如何截取切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
slice1 := []int{1, 2, 3} // len = 3,cap = 3

// 左闭右开
s1 := slice1[0:2]
fmt.Println("s1 = ", s1) // [1 2]

// 原切片和截取后的切片指向同一个内存
s1[0] = 100
fmt.Println("slice1 = ", slice1)
fmt.Println("s1 = ", s1)

// 将 slice 的元素依次拷贝到 s2 中
s2 := make([]int, 3)
copy(s2, slice1)
fmt.Println("s2 = ", s2)
}
在 Go 中,截取切片同样是左闭右开,如果左边参数未指定,那么就从0开始;如果右边参数未指定,那么就到len(slice)

另外需要注意的是原切片和截取后的切片指向的是同一个内存地址,所以如果修改某个切片,另外一个切片也会随之修改。截取后的切片实际上是拷贝了原切片,我们可以使用copy方法进行拷贝

键值对集合 map

下方代码展示了如何声明 map:

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
package main

import "fmt"

func main() {
// 声明 map 类型,key 是 string,value 是 string
var myMap1 map[string]string
if myMap1 == nil {
fmt.Println("myMap1 is nil")
}

// 为 myMap1 分配空间
myMap2 := make(map[string]string)

// 添加键值对
myMap2["one"] = "Java"
myMap2["two"] = "C++"
myMap2["three"] = "Python"
fmt.Println(myMap2)

// 声明 map 类型,并且分配空间,同时赋初始化值
myMap3 := map[int]string{
1: "Java",
2: "C++",
3: "Python",
}
fmt.Println(myMap3)
}

下方代码展示了如何遍历、添加、删除、修改键值对集合map

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
30
31
package main

import "fmt"

func main() {
cityMap := make(map[string]string)

// 添加城市
cityMap["China"] = "Shanghai"
cityMap["USA"] = "New York"
cityMap["Japan"] = "Tokyo"

// 遍历
for key, value := range cityMap {
fmt.Println("key = ", key)
fmt.Println("value = ", value)
}

// 删除
delete(cityMap, "USA")

// 修改
cityMap["Japan"] = "Osaka"

fmt.Println("------------------")
// 遍历
for key, value := range cityMap {
fmt.Println("key = ", key)
fmt.Println("value = ", value)
}
}

结构体 struct

下方代码展示了如何定义一个结构体,并且如何正确地传递参数才能修改成员变量

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
30
31
32
33
34
35
36
37
38
package main

import "fmt"

// 声明一种新的数据类型 myInt,实际是 int 的别名
type myInt int

// 声明一种新的数据类型 Book,实际是 title string 和 author string 结合的结构体
type Book struct {
title string
author string
}

// 值传递
func changeBook(book Book) {
book.title = "Java语言编程实战"
}

// 引用传递
func changeBook1(book *Book) {
book.title = "Java语言编程实战"
}

func main() {
var number myInt = 10
fmt.Printf("type of number = %T,number = %d\n", number, number) // main.myInt 10

book := Book{title: "Go语言编程", author: "ts"}
fmt.Printf("type of book = %T,number = %v\n", book, book)

// 值传递,变量值无法被修改
changeBook(book)
fmt.Printf("type of book = %T,number = %v\n", book, book)

// 引用传递,变量值被修改
changeBook1(&book)
fmt.Printf("type of book = %T,number = %v\n", book, book)
}

封装

下方代码展示了如何打印成员变量,以及实现 Get 和 Set 方法

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
30
31
32
33
34
35
36
37
38
package main

import "fmt"

// 如果类名首字母大写,表示其他包能够访问
type Human struct {
// 如果成员变量首字母大写,表示该成员变量对外能够访问,否则只能在类的内部访问
Name string
Age int
Phone string
}

func (this *Human) Show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Age = ", this.Age)
fmt.Println("Phone = ", this.Phone)
}

func (this *Human) SetPhone(newPhone string) {
this.Phone = newPhone
}

func (this *Human) GetPhone() string {
return this.Phone
}

func main() {
human := Human{Name: "Alice", Age: 30, Phone: "123456789"}

human.Show()

fmt.Println("--------------------")
fmt.Println(human.GetPhone())

fmt.Println("--------------------")
human.SetPhone("987654321")
human.Show()
}
注意在 Get 和 Set 方法中的(this *Human),其中this只是代表它是一个Human指针类型的变量,实际上 Go 中并没有this关键字。另外这是固定写法,表示该函数会被视为对象的 Get 和 Set 方法

继承

在 Go 中如何实现子类对父类的继承,实际上直接在定义子类时添加父类名即可继承父类的成员变量和方法

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import "fmt"

type Human struct {
Name string
Age int
}

func (this *Human) Eat() {
fmt.Println("Human eat...")
}

func (this *Human) Walk() {
fmt.Println("Human walk...")
}

type SuperMan struct {
Human // SuperMan 继承了 Human 类

level int
}

// 重写父类方法
func (this *SuperMan) Walk() {
fmt.Println("SuperMan is walking...")
}

// 定义子类的新方法
func (this *SuperMan) Fly() {
fmt.Println("SuperMan is flying...")
}

func main() {
human := Human{Name: "Alice", Age: 20}
human.Eat()
human.Walk()

fmt.Println("----------------")

// superMan := SuperMan{Human{Name: "Bob", Age: 30}, 1}
var superMan SuperMan
superMan.Name = "Bob"
superMan.Age = 30
superMan.level = 1
superMan.Eat()
superMan.Walk()
superMan.Fly()
}
上述代码还展示了子类如何重新父类方法、以及定义子类的新方法

而在实例子类对象时,有一种写法看上去是将父类对象和子类属性一起传了进去。但可读性较差,推荐使用下面一个个定义属性的方法

多态

接口 interface

在 Go 中实现多态需要使用接口interface来定义父类,注意接口数据类型实际上是个指针,而子类需要全部实现该接口的所有方法才可被视为实现了该接口,此时父类接口类型才指向(或者说引用)子类的具体数据变量

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import "fmt"

// 接口实质上是**指针类型**的
type Animal interface {
Sleep()
GetColor() string // 获取动物的颜色
GetType() string // 获取动物的类型
}

// 具体的类
type Cat struct {
color string
}

func (this *Cat) Sleep() {
fmt.Println("Cat is sleeping")
}

func (this *Cat) GetColor() string {
return this.color
}

func (this *Cat) GetType() string {
return "This is a Cat"
}

// 具体的类
type Dog struct {
color string
}

func (this *Dog) Sleep() {
fmt.Println("Dog is sleeping")
}

func (this *Dog) GetColor() string {
return this.color
}

func (this *Dog) GetType() string {
return "This is a Dog"
}

func showAnimal(animal Animal) {
animal.Sleep()
fmt.Println(animal.GetColor())
fmt.Println(animal.GetType())
}

func main() {
// var animal Animal
// animal = &Cat{"Orange"}
// animal.Sleep()

// animal = &Dog{"Black"}
// animal.Sleep()

cat := Cat{"Orange"}
dog := Dog{"Black"}
showAnimal(&cat)
showAnimal(&dog)
}
本示例代码中定义了一个Animal接口,其中有三个成员方法。另外定义了两个类,只有当子类实现了接口的所有方法,接口指针才指向这个类。

当子类实现了接口,那么我们通过调用父类接口的方法也相当于调用了子类方法。比如示例代码中的showAnimal方法,方法体里虽然是animal.Sleep(),但实际上是在调传进来的对象的Sleep()方法

空接口 interface

interface{}是空接口,是一种通用万能类型,intstringfloat32float64struct都实现了interface{}。实际上因为空接口什么方法都有,所以任何数据类型都实现了空接口

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
package main

import "fmt"

func myFunc(arg interface{}) {
fmt.Println("myFunc is called...")
fmt.Println(arg)

// interface{} 该如何区分此时引用的数据类型是什么?
// 答案:给 interface{} 提供类型断言的机制
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Printf("arg is %T type, and arg = %v\n", value, value)
}
}

type Book struct {
auth string
}

func main() {
book := Book{auth: "Go语言编程"}
myFunc(book)
myFunc("Go语言编程")
myFunc(123)
myFunc(3.14)
}
另外我们可以通过类型断言判断当前空接口引用的数据类型,写法是value, ok := arg.(string),其中arg.(string)有两个返回值,value就是空接口当前引用的变量值,ok是类型断言后的布尔值结果