(持续更新中…)
安装目录清单
- /bin:包含可执行文件,如:编译器,Go 工具
- /doc:包含文档模版
- /lib:包含示例程序,代码工具,本地文档等
- /misc:包含与支持 Go 编辑器有关的配置文件以及 cgo 的示例
- /os_arch:包含标准库的包的对象文件 (.a)
- /src:包含源代码构建脚本和标准库的包的完整源代码(Go 是一门开源语言)
- /src/cmd:包含 Go 和 C 的编译器和命令行脚本
Go特点
- 代码以包(Package)为单元,同一层目录下只能定义一个包名。
- 一个可执行程序有且只能有一个命名为main的包,main包中必须存在main函数。当程序运行时,会查找main函数,并从该函数开始执行;当main函数执行完毕(或跳出该函数)后,可执行程序退出。
- 代码语句可以以“;”结尾,也可以省略。
- 左大括号(“{”)不能另起一行输入,必须与前面的内容同处一行。例如:
func test(){
……
}
- )if、for等关键字之后不需要小括号(C、C++等语言需要小括号)。
- 字符串常量可以使用`来避免转义,例如:
`some content` - 在函数(或方法)的调用语句中使用go关键字可以轻松完成异步编程。例如:
go test()
环境变量
Go语言编译器解压出来即可直接使用,不过,每次运行的时候都要先定位到其所在
的路径,这样使用起来不够方便。因此,配置环境变量是很有必要的。
一般来说,需要以下三个环境变量:
- GOROOT:表示Go语言编译器、文档以及相关工具所在的路径,即1.1节中解压缩出来的go目录所在的路径。假设把文件解压到D:\tools目录下,那么,GOROOT环境变量应设置为D:\tools\go。
- GOPATH:表示Go语言编译器的工作目录。此目录下面要求包含三个子目录——src、bin、pkg。在编译代码时,Go语言编译器会在src目录中查找代码文件,编译后生成的可执行程序将输出到bin目录中。程序包(Package)会输出到pkg目录中。
- path:把编译器的可执行文件所在的目录路径追加到path变量末尾。当执行go命令进行编译时,就不需要手动去定位编译器所在的路径,系统会自动查找到该程序。
代码结构
Go语言的代码文件的扩展名为.go,本质上是一个文本文件,因此任何文本编辑器(如记事本程序)都可以用来编写Go代码。当然,为了提高代码的编写效率,许多开发人员会选择一款自己喜欢的专用编辑器(如GoLand、LiteIDE、VisualStudio Code等)。
先看一个简单的Go应用程序
package main
import "fmt"
func main() {
fmt.Print("我的程序")
}
第一行使用package语句声明此代码文件所属的包名,所有Go代码文件都要写上这一行。上述例子中,为代码分配了名为main的包名(Package Name)。
接下来是一条import(导入)语句,它告诉编译器该文件中的代码将用到哪个包里面的API。在上述例子中,导入了fmt包。
package语句是必需的,而import语句是可选的,只有当需要使用到其他包时才会进行导入。
然后使用func关键字定义了一个函数,名为main。在main函数内部,调用了fmt包中的Print函数。Print函数的作用是向屏幕输出(打印)一条文本消息,上述例子运行后会输出文本“我的应用程序”
语法
main包与main函数
命名为main的包在编译时会生成可执行文件,可以直接运行。在Windows操作系统中,可执行文件的扩展名为.exe,而Linux中的可执行文件无扩展名。main包中必须存在一个名为main的函数,作为程序的入口点。入口点是应用程序运行时的起点,代码指令会进入main函数;当程序指令跳出main函数后,整个应用程序就结束了,进而退出当前进程。
由于Go语言允许将一个包的代码分散在多个代码文件中(前提是这些文件必须处于同一级目录下),在编写代码时就有可能出现main函数重复定义的问题。
在一个应用程序中,main函数只允许出现一次。在同一个代码文件中一般不容易出现此错误,但当多个代码文件同属一个包
时,就容易出现此错误(开发人员忘记前面已定义过main函数)。
代码块
必须写成
func main() {
fmt.Print("我的程序")
}
if a > 0{
}else{
}
而不是
func main()
{
fmt.Print("我的程序")
}
if a > 0{
}
else{
}
go语言编译器
执行go命令时,可以使用build参数来编译代码。如果build参数后面没有附加任何参数,那么go程序会编译当前目录下的所有代码文件。
go build 文件名.go ....
如果不填写文件名则是编译所有文件
如果想修改编译后的文件名,则可以使用以下指令
许多时候,开发人员希望编译后马上运行应用程序。这种情况可以改用run参数,使用方法如下:
由于run参数要求在编译之后运行程序,所以指定的代码文件必须声明为main包。
如
go run test.go
赋值
使用:=
(英文的冒号和等号组合而成)运算符可以直接向新变量赋值,不需要用var关键字来声明。
运算符
只要两个变量的值相等,那么他们就是相等的,结构体也如此,如果两个结构体类型内的变量值完全相同,那么两者就是相同的
结果是相同的,如果两个指针指向同一个变量的地址,那么它们也是相同的
答案是相同的
var a int16 = 29156
var r = a << 7
fmt.Printf("%[1]d(%016[1]b) << 7: %[2]d(%016[2]b)", a, r)
[x]表示输出第x个数(在逗号后面的数)
对于按位清零运算符(&^)
它的作用是对左操作数的每一位,如果右操作数的对应位是 1,则将左操作数的对应位清零(即设为 0);如果右操作数的对应位是 0,则保留左操作数的对应位。
a&^b = 0100->4
生疏点
目录结构
Go语言是以目录为单位来界定程序包的,因此,在同一级目录下只允许使用一个包名。一个包则可以分布在多个代码文件中。假设foo目录下有a.go、b.go两个文件,其代码
如下:
如下是错误的
import语句
注意:import语句只指定包所在的目录路径,包的名称将通过代码文件中的package语句来识别。
test1和test2是文件夹的名字,里面的包名可能不为test1和test2
这种引用文件夹的操作要在src目录下进行
可以起别名
可以别名都不用
直接使用包内的函数即可,不用加包名.
模块mod
go mod init mod_name
在Go语言中,go mod init命令用于初始化一个新的模块(module),并且创建一个go.mod文件来管理模块的依赖关系。
具体来说,执行go mod init helloworld命令将创建一个名为helloworld的新模块,并生成一个go.mod文件,该文件记录了当前项目的模块路径(通常是您的代码托管服务的地址),以及任何依赖的模块及其版本。这有助于确保您的项目能够在不同环境中正确地构建和运行,并且可以更好地管理依赖关系。
一旦执行了go mod init,您就可以使用go mod命令来管理您的项目的依赖关系,例如使用go mod tidy来清理不再使用的依赖项,或者使用go mod vendor来将依赖项复制到您的项目中的vendor目录中。
引用
一个正常的go.mod文件:
项目结构
在包含各包的文件夹里使用go mod init main(也可以是别的 比如example.com/main)
之后引用内部各包的时候
"import main/name or import example.com/main/name"
变量名与私有的关系
首字母大写的函数,变量都是私有变量,其他包访问不到
结构体变量里的参数如果首字母大写,同样是私有变量
匿名变量
对于函数的某些返回值,如果不需要时,则可以使用匿名变量_ 接收
整数常量的表示方式
例子:
如果太长,可以用_分段表示,称为分隔符
但是,分隔符不能出现在数值的开头,也不能出现在结尾。
Timer类型
Timer是一种特殊的计时器,当指定的时间到期后,会将当前时间发送到其C字段中。C字段是只读的通道类型(<-chan Time),其他协程(Go routines)将通过C字段接收Time实例(计时器过期时所设置的时间)。
<-timer.C
的意义
当你写 <-timer.C
时,你在等待从 timer.C 通道接收一个值。这个操作会阻塞当前 goroutine,直到计时器到期并向 C 通道发送一个值。
使用场景
package main
import (
"fmt"
"time"
)
func main() {
timeout := time.NewTimer(5 * time.Second)
done := make(chan bool)
go func() {
// 模拟长时间运行的操作
time.Sleep(3 * time.Second)
done <- true
}()
select {
case <-done:
fmt.Println("Operation completed within the timeout")
case <-timeout.C:
fmt.Println("Operation timed out")
}
}
在这个例子中,select 语句用于同时等待操作完成或计时器到期。由于操作在3秒内完成,程序会打印 Operation completed within the timeout。哪个通道先接收到了消息,就进入了对应的case(消息是当前的时间信息,如:2024-05-24 20:43:10.2986142 +0800 CST m=+1.007609201
通过使用 time.Timer 和通道,Go 提供了一种简单且强大的方式来处理并发操作中的时间控制。
定义的函数中,各部分的含义
对于如下函数
func (o test) setTag(v string) {
o.tag = v
}
- 接收者 (o test)
接收者定义了该方法所属的类型,并允许该方法与该类型的值或指针关联。接收者可以是类型的值或指针。
(o test) 表示 setTag 方法是类型 test 的一个方法。o 是接收者的名字,类似于方法内的局部变量。
通过这种方式,可以在类型 test 上定义方法,从而允许你调用 test 类型的变量的这些方法。
接收者有两种类型:值接收者和指针接收者。这里的接收者是值接收者,意味着调用该方法时,接收者的副本将传递给方法。如果你需要在方法中修改接收者的值,应该使用指针接收者。 - 参数 (v string)
参数是方法调用时传递给方法的输入值。在这个例子中,v 是一个字符串类型的参数。
(v string) 表示 setTag 方法接受一个字符串类型的参数 v。
下面是一个使用的例子:
func (o test) setTag(v string) {
o.tag = v
}
func main() {
t := test{tag: "initial"}
t.setTag("new")
fmt.Println(t.tag) // 输出 "initial" 而不是 "new"
}
如果使用指针接收者
func (o *test) setTag(v string) {
o.tag = v
}
func main() {
t := test{tag: "initial"}
t.setTag("new")
fmt.Println(t.tag) // 输出 "new"
}
在这个例子中,setTag 方法的接收者是 *test,表示接收者是 test 类型的指针。这样,在方法中对接收者的任何修改都会影响原始变量 t。
使用值接收者和指针接收者的选择取决于你是否需要在方法中修改接收者的值。如果需要修改接收者的值,应使用指针接收者。
iota常量
尽管iota的值为0,但是在定义常量时可以使用算术表达式。例如,下面代码中,H的值为1,随后I、J、K的值依次递增。
在批量定义常量的代码块中,iota常量并没有要求在第一个常量中使用,它可以出现在常量列表的任何位置,请看下面代码:
当iota出现在常量列表的首位置时,它的值为0,但随着出现的位置不同,iota常量的值会改变。当定义W常量时,iota的值为4(第五个常量,从0开始计算),iota+3使得W的值为7。X、Y、Z的值依次加1,这相当于:
函数的多个返回值
Go语言支持函数返回多个值。多个返回值的定义方法与参数相同,但返回值列表允许省略变量名称,如:
如果返回值已命名,可以选择性地为它们赋值(未赋值的返回值将使用类型默认值,例如int类型的默认值为0),但是,在函数体的末尾必须有return语句。例如,getSomeStrings
函数返回两个string类型的值——a、b。
return a,b也可以
函数的可变参数
可变个数的参数只能出现在参数列表的末尾
调用函数时,可以传入多个string类型变量
可变个数参数的类型为“切片”(slice),它是以数组为存储基础的集合类型。在函数体内部,可以使用len函数来获取可变参数的个数,也可以使用for range语句来循环访问每一个元素。
由于可变个数的参数是切片类型,可以先初始化一个切片实例,然后再把该实例传递给args参数。此处要注意的是,nums变量在传递时要在后面加上“…”以区别于普通参数。
函数作为函数传递
匿名函数
switch语句
case子句还可以指定多个值,只要其中有一个值与参考表达式匹配,该case子句中的代码就会执行。
如果switch子句中的表达式被省略,那么分支判断工作就会下发到各个case子句中进行。
这种情况下,case子句的计算结果应为布尔类型的值。true表示条件匹配,false表示不匹配。在上面代码中,整数72可被3整除,因此,第二个case子句能顺利匹配,输出“72能被3整除”。
需要注意的是,如果一个变量同时满足case里的多个条件,那么只会执行第一个满足的case。
类型断言和 type switch
类型断言用于从接口类型断言具体类型。通常,类型断言的基本形式如下:
v := c.(T)
其中:
c
是一个接口类型的变量。T
是你要断言的具体类型。- 如果
c
保存的值是类型T
,则断言成功,v
会是T
类型的值。 - 如果断言失败,程序会产生一个恐慌(panic)。
为了安全地进行类型断言,可以使用逗号 , ok
的形式:
v, ok := c.(T)
if ok {
// 断言成功,v 是 T 类型
} else {
// 断言失败,c 不是 T 类型
}
type switch
type switch
是一种特殊的 switch 语句,用于根据接口变量保存的具体类型执行不同的分支。type switch
使用 .(type)
语法来判断接口变量的具体类型。
以下是 type switch
的语法和使用示例:
switch v := c.(type) {
case int:
fmt.Printf("c is an int: %d\n", v)
case string:
fmt.Printf("c is a string: %s\n", v)
case bool:
fmt.Printf("c is a bool: %t\n", v)
default:
fmt.Printf("unknown type\n")
}
c.(type)
用在type switch
中,用于获取接口变量c
的具体类型。v := c.(type)
是type switch
语法的一部分,在每个case
分支中,v
被赋值为c
转换后的具体类型值。- 每个
case
分支检查c
是否是指定的类型,如果是,则执行对应的代码块,并将c
转换为该类型赋值给v
。
示例代码
以下是一个完整的示例代码,演示了如何使用 type switch
:
package main
import (
"fmt"
)
func main() {
var c interface{}
c = 42
checkType(c)
c = "hello"
checkType(c)
c = true
checkType(c)
c = 3.14
checkType(c)
}
func checkType(c interface{}) {
switch v := c.(type) {
case int:
fmt.Printf("c is an int: %d\n", v)
case string:
fmt.Printf("c is a string: %s\n", v)
case bool:
fmt.Printf("c is a bool: %t\n", v)
default:
fmt.Printf("unknown type\n")
}
}
关键点总结
v := c.(type)
语句只能在type switch
中使用,不能单独使用。type switch
用于根据接口变量保存的具体类型执行不同的代码分支。- 每个
case
分支中,v
被赋值为c
转换后的具体类型值,允许在分支内以具体类型操作v
。
通过使用 type switch
,可以简洁、安全地处理接口变量的不同具体类型。
配合多态的用法
定义一个接口
用三种不同结构体去实现接口,之后可以定义一个类型为tester的对象,用不同的结构体赋值给它
var s tester = data3{}
fallthrough
如果在某个case子句代码的最后出现fallthrough语句,那么紧跟在该case子句后的代码就会被执行。
变量number的值为200,与第一个case子句匹配,于是输出“分支一”。随后,遇到fallthrough语句,使得第二个case子句也执行了,继而输出“分支二”。fallthrough语句会将代码直接跳到第二个case子句的代码中,不管它与number变量的值是否相等。
fallthrough语句必须是case子句的最后一行代码,下面这种写法会发生编译错误。
Continue和break
此语法对break同样有效
结构体
若希望结构体的字段成员能被其他包的代码访问,除了结构体自身的名称需要首字母大写外,其字段成员的名称也要首字母大写。例如下面代码所定义的Order结构体,它的所有字段都能在其他包中访问。
命名中首字母为小写的字段只能在当前包中访问
在多行初始化语句中,最后一个字段末尾的逗号不能省略。
结构体函数
结构体的方法对象并不是在结构体的内部定义的,而是在结构体外部以函数的形式定义。例如:
如果要实际修改对象内遍历的值,则传入的对象类型要是指针:
使用第一个函数不会改变对象内data的值,而第二个函数可以
调用setIntV1方法时,demo实例会将自身复制一份再传递给方法,所以在setIntV1方法内部所修改的是demo实例副本的data字段,而不是原来demo实例(变量a所引用的对象)的data字段。这使得setIntV1方法被调用后,a.data保持原值(100)不变。
调用setIntV2方法时,demo实例会将自身的内存地址传递给方法,在方法内部所修改的data字段属于原来的demo实例(变量a所引用的对象)。这期间操作的都是同一个实例,因此在调用完setIntV2方法后,a.data的值会被更新为200。
可以得出结论:当需要在方法内部修改结构体对象的字段时,应该传递该结构体实例的指针。如果在方法内部只是读取结构体字段的值,那么传递给方法的结构体实例可以使用指针类型,也可以不使用指针类型。
结构体嵌套
与其他面向对象的编程语言不同,Go语言的类型不能进行继承,但可以嵌套,通过
类型嵌套也能实现类似于继承的效果
也可以省略base而直接访问其字段成员。
如x.code
接口也可以嵌套
iFile1接口定义了getFilename方法,iFile2接口定义了getTypeExt方法,并且iFile1接口内嵌到了iFile2接口中。
fileInfo结构体包含两个方法:getFilename、getTypeExt。
由于iFile2接口中内嵌了iFile1接口,定义为iFile2类型的变量也可以调用getFilename方法
接口定义
方法不能重载,实现方法所对应的参数类型和返回值必须完全一致
在Go语言中,接口定义了一组方法,而任何实现这些方法的类型都可以被视为该接口的实现。
为了创建一个接口对象并将实现该接口的结构体实例赋值给它,您需要按照以下步骤进行操作。
示例代码
假设我们有一个名为 Locker
的接口和一个实现了该接口的结构体 Safe
:
package main
import (
"fmt"
)
// 定义接口 Locker
type Locker interface {
Lock()
Unlock()
}
// 定义结构体 Safe
type Safe struct {
locked bool
}
// 实现接口 Locker 的 Lock 方法
func (s *Safe) Lock() {
s.locked = true
fmt.Println("Safe is locked")
}
// 实现接口 Locker 的 Unlock 方法
func (s *Safe) Unlock() {
s.locked = false
fmt.Println("Safe is unlocked")
}
func main() {
// 创建 Safe 的实例
mySafe := &Safe{}
// 声明一个 Locker 类型的接口变量
var myLocker Locker
// 将 Safe 的实例赋值给 Locker 接口变量
myLocker = mySafe
// 使用接口对象调用实现的方法
myLocker.Lock()
myLocker.Unlock()
}
解释
- 定义接口
type Locker interface {
Lock()
Unlock()
}
接口 Locker
定义了两个方法 Lock
和 Unlock
。
- 定义结构体并实现接口
type Safe struct {
locked bool
}
func (s *Safe) Lock() {
s.locked = true
fmt.Println("Safe is locked")
}
func (s *Safe) Unlock() {
s.locked = false
fmt.Println("Safe is unlocked")
}
结构体 Safe
实现了 Locker
接口中的 Lock
和 Unlock
方法。
- 创建接口对象并赋值
// 创建 Safe 的实例
mySafe := &Safe{}
// 声明一个 Locker 类型的接口变量
var myLocker Locker
// 将 Safe 的实例赋值给 Locker 接口变量
myLocker = mySafe
// 使用接口对象调用实现的方法
myLocker.Lock()
myLocker.Unlock()
在 main
函数中:
- 我们创建了一个
Safe
类型的实例mySafe
。 - 声明了一个
Locker
类型的接口变量myLocker
。 - 将
mySafe
赋值给myLocker
。由于Safe
实现了Locker
接口中的所有方法,因此mySafe
可以赋值给myLocker
。 - 使用接口变量
myLocker
调用了Lock
和Unlock
方法。
注意事项
- 实现接口的方法必须有相同的签名,包括方法名、参数和返回值。
- 在赋值时,确保结构体的实例是指针类型(如
&Safe{}
),以便修改结构体的状态。如果使用值类型(如Safe{}
),会导致方法操作的对象是副本而非原对象。(实现接口的时候必须是使用结构体的指针传入才可以赋值 - 接口变量在赋值后,可以调用其方法,但不能访问实现类型的具体字段。
通过这种方式,您可以创建一个接口对象并将实现该接口的结构体实例赋值给它,并使用接口对象调用实现的方法。
切片
和python中的一致,左闭右开
从同一个数组实例产生的所有切片实例都会共享数组中的元素,也就是说,当数组中的元素被更改,切片中对应的元素也会同步更新;反过来,如果切片中的元素被更改,数组中对应的元素也会同步更新。
双向链表List
对于Element指针类型,可以使用函数Prev()和Next()方法上一个元素和下一个
list因为是小写不能直接访问
Value因为是大写,所以可以使用.Value直接访问
List也是一个结构体,如下:
前4个方法会返回指向新元素的指针
List有一个虚拟头节点,不参与实际访问
package main
import (
"container/list"
"fmt"
)
func printElems(ls *list.List){
for x:=ls.Front();x!=nil;x=x.Next(){
fmt.Printf("%v ",x.Value)
}
fmt.Println()
}
func main() {
var mylist = list.New()
mylist.PushFront(100)
for i:=200;i<=400;i+=100{
mylist.PushBack(i)
}
printElems(mylist)
lastsecond :=mylist.Back().Prev()
mylist.InsertBefore(700,lastsecond)
newElem:=mylist.InsertAfter(111,lastsecond)
fmt.Printf("%v\n",newElem.Value)
printElems(mylist)
}
从前往后删除元素和从后往前删除元素
for e := mylist.Front(); e != nil; e = mylist.Front() {
tmp := mylist.Remove(e)
fmt.Printf("已删除:%v\n", tmp)
}
for e := mylist.Back(); e != nil; e = mylist.Back() {
tmp := mylist.Remove(e)
fmt.Printf("已删除:%v\n", tmp)
}
环形链表Ring
参数n指定环形链表中的元素个数,返回的Ring指针表示链表的当前位置,默认是第
一个元素。
Ring结构体还包含以下方法:
(1)Len:返回环形链表中元素的个数。
(2)Next:返回后一个元素的引用。
(3)Prev:返回前一个元素的引用。
(4)Move:滚动环形链表中的元素。滚动的元素个数为n % r.Len(),即传入参数
n除以链表元素个数后的余数。这是因为环形链表的元素是循环访问的,取余的目的是排
除重复的“圈数”,得到实际移动的元素个数。
(5)Link:将另一个环形链表链接到当前链表中,形成新的链表。
(6)Unlik:从当前链表中解除n个元素的链接。n的实际使用值也是n % r. Len(),
同理也是为了排除重复的“圈数”。
(7)Do:指定一个自定义函数,环形链表会为每个元素调用一次该函数,并把元素
的值传递给函数。
例子
func main() {
var myring = ring.New(5)
n := myring.Len()
p := myring
v := 'A'
for x := 0; x < n; x++ {
p.Value = v
p = p.Next()
v++
}
p = myring
for x := 0; x < 2*myring.Len(); x++ {
fmt.Printf("%c ", p.Value)
p = p.Next()
}
}
滚动环形链表
使用Move函数
调用Move方法可以让环形链表滚动指定数量的元素,并且返回目标元素的指针。由
于环形链表不区分首尾元素,为了排除重复的循环,实际被滚动的元素个数会变为n % Len()
。如果n≥0,表示元素向前滚动;否则元素将向后滚动。
环形链表中有4个元素,元素值依次为1、2、3、4。变量myring指向元素1,链表向后滚动
18个元素。由于18 % 4的结果为2,所以链表向后滚动过程中,有4次重新回到元素1,之
后再向后滚动两个元素。因此最终返回的是元素3的指针
func main() {
var myring = ring.New(4)
p := myring
for i := 0; i < myring.Len(); i++ {
p.Value = i + 1
p = p.Next()
}
rx := myring.Move(18)
fmt.Printf("%v", rx.Value)
}
Move内可以填负数,反向滚动
反射
使用reflect包
Type类型里的NumMethod()方法只会返回大写字母开头的函数数量
在字段信息中,PkgPath成员比较特殊。如果被枚举的字段成员是公共成员(首字母大写),那么PkgPath的值将被设置为空白字符串;如果此字段成员是非公共成员(首字母小写),那么PkgPath的值将表示该字段所在包的路径。
通道
其源代码如下:
type ChanDir int
const (
RecvDir ChanDir = 1 << iota // <-chan
SendDir // chan<-
BothDir = RecvDir | SendDir // chan
)
示例如下:
如何判断创建的是什么类型的通道?
看什么对象给chan进行了什么操作
如,C1就是双向通道
C2,因为是bool传输给chan通道,所以是只发送数据的通道,因为它发给了chan
C3是只接收数据的通道,因为chan发出的数据被接收了,且只能接收uint类型的
var(
C1 chan uint
C2 chan<-bool
C3 <-chan uint = C1
)
func recognize(c interface{}){
t:=reflect.TypeOf(c)
fmt.Printf("通道类型:%s\n",t.Name())
fmt.Printf("通信方向:")
switch t.ChanDir(){
case reflect.RecvDir:
fmt.Printf("只能从通道接收数据\n")
case reflect.SendDir:
fmt.Printf("只能从通道发送数据\n")
case reflect.BothDir:
fmt.Printf("可以从通道发送和接收数据\n")
}
}
func main() {
recognize(C1)
recognize(C2)
recognize(C3)
}
判断结构体是否继承了某个接口
使用reflect包里的Implements方法
type ball interface {
Play()
}
type ggball interface {
Do()
}
type footBall struct{}
func (b footBall) Play() {
fmt.Printf("踢足球\n")
func main() {
tball := reflect.TypeOf(new(ball)).Elem()
tggball := reflect.TypeOf(new(ggball)).Elem()
fmt.Printf("tball类型是:%s\n", tball.Kind())
tfb := reflect.TypeOf(footBall{})
b := tfb.Implements(tball)
fmt.Printf("结构体%s\n", tfb.Name())
if b {
fmt.Printf("实现了")
} else {
fmt.Printf("未实现")
}
fmt.Printf("%s 接口\n", tball.Name())
b = tfb.Implements(tggball)
fmt.Printf("结构体%s\n", tfb.Name())
if b {
fmt.Printf("实现了")
} else {
fmt.Printf("未实现")
}
fmt.Printf("%s 接口\n", tggball.Name())
}