文章目录
- go语言指针
- go语言方法
- go语言接口
- 什么是接口
- 接口定义
-
- [接口里装的是"一个具体值",phone.(Apple) 取出来的也是"这个值的拷贝",改它不会改回接口里那份;而用指针时,接口里装的是地址,你改的是同一块内存。](#接口里装的是“一个具体值”,phone.(Apple) 取出来的也是“这个值的拷贝”,改它不会改回接口里那份;而用指针时,接口里装的是地址,你改的是同一块内存。)
-
- [1)phone = Apple{}:接口里装的是一个"Apple 值"](#1)phone = Apple{}:接口里装的是一个“Apple 值”)
- [2)为什么 phone.(Apple).PhoneName = "Apple" 直接写还不行?](#2)为什么 phone.(Apple).PhoneName = "Apple" 直接写还不行?)
- 实现多个接口
- 空接口
- 断言
- 接口作函数参数
- 接口嵌套
go语言指针
什么是指针
像C语言一样,go语言也有指针的概念。简单理解,指针就是地址,指针变量就是存放地址的变量。在一个变量前加上*,那么这个变量就是指针变量,指针变量只能存放地址。
1个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。
创建指针
声明指针
我们可以像声明其它类型的变量一样,声明一个指针变量
go
var a *int
上述方法声明了一个整形指针变量a,但是该变量还没有存放任何地址,操作系统并未为其指向的内容分配内存,即该指针没有指向任何内容,所以它是一个空指针。
go
var b int = 3
a = &b
此时不再是一个空指针,a指向一个整型变量,该变量的值是3。
要获取指针变量所指向的内容,很简单,在变量前加*即可
代码展示:
go
package main
import "fmt"
func main() {
var a *int
fmt.Printf("赋值前a:%v\n", a)
var b int = 3
a = &b // a存放的是b的地址
fmt.Printf("赋值后a:%v\n", a) // 打印出的地址
fmt.Printf("a指向的内容是:%v\n", *a) // *a表示a存放的地址对应的内存存放的内容,这里为整数3
}
运行结果:
bash
赋值前a:<nil>
赋值后a:0xc00001c040
a指向的内容是:3
new() 函数
Go语言还提供了另外一种方法来创建指针变量,使用new函数创建
go
new(type)
代码展示:
go
package main
import "fmt"
func main() {
s := new(string)
fmt.Printf("赋值前s:%v\n", s)
fmt.Printf("赋值前s的内容:%v\n", *s)
*s = "Golang学习之路"
fmt.Printf("赋值后s:%v\n", s)
fmt.Printf("赋值后s的内容:%v\n", *s)
}
运行结果:
bash
赋值前s:0xc00009e210
赋值前s的内容:
赋值后s:0xc00009e210
赋值后s的内容:Golang学习之路
可以看到,通过new函数创建指针的时候,系统会为其分配内存,所以两次都能打印出地址,但是,在没有对其赋值的时候,系统会存放默认值,string类型的就是空字符串""。
go语言方法
方法的定义
方法是一类特殊的函数,方法是绑定在某种类型的变量上的函数,它有一定的约束范围。而变量的类型不仅仅局限于结构体类型,可以是任意类型,比如int、bool等的别名类型。
有过c++或者java基础的同学可以这样理解,一个struct加上绑定在这个类型上的方法就等价于一个类。
方法的定义格式如下
go
func (recv recv_type) methodName(paramr_list) (return_val_list) { ... }
对比前面我们学习过的函数,方法跟函数的唯一区别就是在func关键字和函数名之间加了一个接收者类型和接收者,同样,这个接收者的类型可以是普通变量或者指针变量
方法的调用跟结构体属性的调用方式相同,都是通过 . 操作符来完成
下面给Student结构定义一个方法
go
package main
import "fmt"
type Student struct {
ID int
Name string
Age int
Score int
}
func (st *Student) GetName() string {
return st.Name
}
func main() {
st := &Student{
ID: 100,
Name: "zhangsan",
Age: 18,
Score: 98,
}
fmt.Printf("学生st的姓名是:%s\n", st.GetName()) // 调用Student的方法GetName
}
运行结果:
bash
学生st的姓名是: zhangsan
注意:在 go语言中,类型的定义和绑定在它上面的方法的定义可以不放置在同一个文件中,它们可以存在在不同的源文件 ,唯一的要求是:它们必须在同一个包下面
方法的调用
定义在指针类型或者值类型上的方法可以通过指针变量或者值变量来调用,可能读起来有点绕,下面看例子,会更清晰
go
package main
import "fmt"
type Student struct {
ID int
Name string
Age int
Score int
}
func (st *Student) SetScore(score int) {
st.Score = score
}
func (st Student) GetScore() int {
return st.Score
}
func main() {
st := &Student{
ID: 100,
Name: "zhangsan",
Age: 18,
Score: 98,
}
fmt.Printf("设置前, 学生st的分数是:%d\n", st.GetScore()) // 通过指针调用定义在值类型的方法GetScore
st.SetScore(100) // 通过指针调用定义在指针类型上的方法
fmt.Printf("设置后, 学生st的分数是:%d\n", st.GetScore()) // 通过指针调用定义在值类型的方法GetScore
}
运行结果:
bash
设置前, 学生st的分数是:98
设置后, 学生st的分数是:100
继承
go语言不像c++或者java一样有显示的继承关系,因为go语言没有类的概念,所以自然就不存在父类、子类一说,自然就没有继承的关系。那么,go语言是如何实现功能的呢?
答案是组合,go语言中不论是属性,还是方法的继承都是通过组合来实现的
请看下面例子
go
type People struct {
Name string
Age int
}
type Student struct {
ID int
Score int
People // 将People作为Student的一个属性,注意不要加变量名,这就是隐式继承
}
隐式继承 vs 显式继承


将原来Student的定义改为Student 和People 两个结构来定义,通过这种组合的方式,其实Student 就拥有了 People的Name属性和Age属性
go
package main
import "fmt"
type People struct {
Name string
Age int
}
func (p *People) GetName() string {
return p.Name
}
type Student struct {
ID int
Score int
People // 将People作为Student的一个属性,注意不要加变量名,这就是隐式继承
}
func (st *Student) SetScore(score int) {
st.Score = score
}
func (st Student) GetScore() int {
return st.Score
}
func main() {
st := &Student{
ID: 100,
Score: 98,
People: People{
Name: "zhangsan",
Age: 18,
},
}
fmt.Printf("学生st的分数是:%d\n", st.GetScore())
fmt.Printf("学生st的姓名是:%s\n", st.GetName())
}
运行结果:
bash
学生st的分数是:98
学生st的姓名是:zhangsan
这个例子中,Student 通过内嵌People获得了People的Name和Age属性以及GetName方法。
go语言接口
什么是接口
go语言中的接口很简单,就是一组方法的声明。当某一种类型实现了所有这些声明的方法,那么就称这种类型为该接口的一种实现。
接口定义
go语言中同样用关键字 interface 来定义接口
go
type interfaceName interface {
methodName1([parameter_list]) [return_type_list]
methodName2([parameter_list]) [return_type_list]
methodName3([parameter_list]) [return_type_list]
...
}
下面看一个具体的interface的例子
go
package main
import "fmt"
type Phone interface {
Call()
SendMessage()
}
type Apple struct{
PhoneName string
}
func (a Apple) Call() {
fmt.Printf("%s有打电话功能\n", a.PhoneName)
}
func (a Apple) SendMessage() {
fmt.Printf("%s有发短信功能\n", a.PhoneName)
}
type HuaWei struct{
PhoneName string
}
func (h HuaWei) Call() {
fmt.Printf("%s有打电话功能\n", h.PhoneName)
}
func (h HuaWei) SendMessage() {
fmt.Printf("%s有发短信功能\n", h.PhoneName)
}
func main() {
a := Apple{"apple"}
b := HuaWei{"huawei"}
a.Call()
a.SendMessage()
b.Call()
b.SendMessage()
var phone Phone // 声明一个接口类型phone
phone = new(Apple) // 注意这种创建方式,new函数参数是接口的实现
phone.(*Apple).PhoneName = "Apple" // 这里使用断言给phone的成员赋值,后面会讲到接口的断言
phone.Call()
phone.SendMessage()
}
运行结果:
bash
apple有打电话功能
apple有发短信功能
huawei有打电话功能
huawei有发短信功能
Apple有打电话功能
Apple有发短信功能
上述声明了一个phone的接口,有两个方法Call()和Sendmessage(),然后定义了两个结构Apple和HuaWei,这两个结构都实现了Phone接口定义的所有方法,但是实现的方法不同,所以Apple和HuaWei是Phone接口的两种实现。
注意:上述例子的phone变量的定义,首先声明phone为一个接口,接着用new方法为这个phone定义,注意,这里new的参数必须是phone的一种实现,假设这里Apple结构没有实现SendMessage方法,那么Apple结构就不是Phone接口的一个实现,上述代码的45行用new定义phone的时候就会报错。
接口里装的是"一个具体值",phone.(Apple) 取出来的也是"这个值的拷贝",改它不会改回接口里那份;而用指针时,接口里装的是地址,你改的是同一块内存。
1)phone = Apple{}:接口里装的是一个"Apple 值"
go
var phone Phone
phone = Apple{PhoneName: "old"} // 接口内部保存:动态类型=Apple,动态值=一份 Apple 的拷贝
这时做断言:
go
a := phone.(Apple) // a 得到的是"接口里那份 Apple 值"的拷贝
a.PhoneName = "Apple"

2)为什么 phone.(Apple).PhoneName = "Apple" 直接写还不行?
因为 phone.(Apple) 得到的是一个非地址的临时值(不可寻址),Go 不允许对"不可寻址的值"直接改字段:
go
phone.(Apple).PhoneName = "Apple" // 编译报错:cannot assign to struct field ... in map / not addressable 之类
你必须先接出来到变量里(但接出来仍是拷贝):
go
tmp := phone.(Apple)
tmp.PhoneName = "Apple" // 只改 tmp
phone = tmp // 想让接口里的值变,必须再塞回去


实现多个接口
有上述接口的定义我们知道,类型可以实现接口,那么一种类型可以实现多个接口吗,答案是:可以的。
请看下面例子:
go
package main
import "fmt"
type MyWriter interface {
MyWriter(s string)
}
type MyRead interface {
MyReader()
}
type MyWriteReader struct {
}
func (r MyWriteReader) MyWriter(s string) {
fmt.Printf("call MyWriteReader MyWriter %s\n", s)
}
func (r MyWriteReader) MyReader() {
fmt.Printf("call MyWriteReader MyReader\n")
}
func main() {
var myRead MyRead
myRead = new(MyWriteReader)
myRead.MyReader()
var myWriter MyWriter
myWriter = MyWriteReader{}
myWriter.MyWriter("hello")
}
运行结果:
bash
call MyWriteReader MyReader
call MyWriteReader MyWriter hello
上述例子我们定义了一个 MyWriter 接口,该接口有一个方法 MyWriter 方法接受一个 string 类型的参数。接着,我们定义了一个 MyReader 接口,该接口有一个方法 MyReader。然后定义了一个 ReadWriter结构,实现了接口 MyWriter 和接口 MyReader 中的所有方法,所以ReadWriter分别实现了两个不同的接口MyWriter和MyReader。
空接口
没有任何方法声明的接口称之为空接口,interface{}
所有的类型都实现了空接口,因此空接口可以存储任意类型的数值
Golang 很多库的源代码都会以空接口作为参数,表示接受任意类型的参数,比如 fmt 包下的 Print 系列函数
go
func Println(a ...interface{}) (n int, err error)
func Print(a ...interface{}) (n int, err error)
看下面例子:
go
package main
import (
"fmt"
)
func main() {
var any interface{}
any = 10
fmt.Println(any)
any = "golang"
fmt.Println(any)
any = true
fmt.Println(any)
}
运行结果:
bash
10
golang
true
上述例子首先声明了一个空接口any,首先用来存储整形变量10,然后又用来存储字符串golang,最后用来存储布尔型变量true,所以空接口可以存储任意类型的数值 。fmt.Println函数的参数是空接口类型interface{},所以能将结果正确打印出来。
断言
在介绍断言之前,先看一个例子
go
package main
func main() {
var a int = 1
var i interface{} = a
var b int = i
}
运行结果:
bash
./prog.go:6:14: cannot use i (variable of type interface{}) as type int in variable declaration: need type assertion
编译报错,不能将interface{}类型的变量i赋值给整型变量b
所以在写代码的时候我们需要注意,可以将任意类型的变量赋值给空接口interface{}类型,但是反过来不行。
那为了让这个操作能够完成,我们需要怎么做呢?就是断言
类型断言(Type Assertion)接口操作,用来检查接口变量的值是否实现了某个接口或者是否是某个具体的类型
断言的一般格式为:
go
value, ok := x.(T) // x为接口类型,ok为bool类型
- T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的值,其类型是 T。
- x 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,返回值是一个类型为 T 的接口值。
- 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
请看下面例子:
go
package main
import (
"fmt"
)
func main() {
var x interface{}
x = 8
val, ok := x.(int)
fmt.Printf("val is %d, ok is %t\n", val, ok)
}
运行结果:
bash
val is 8, ok is true
注意:如果在断言的过程中,没有 bool 这个值的判断,如果断言成功程序正常运行,假设 interface{} 存储的值跟要断言的类型不一致,则程序会报 panic。
看下面例子:
go
package main
import (
"fmt"
)
func main() {
var x interface{}
x = "golang"
val := x.(int)
fmt.Println(val)
}
运行结果
go
panic: interface conversion: interface {} is string, not int
接口作函数参数
接口做函数参数,在函数定义的时候,形参为接口类型 ,在函数调用的时候,实参为该接口的具体实现。
具体看下面例子:
go
package main
import "fmt"
type Reader interface {
Read() int
}
type MyReader struct {
a, b int
}
func (m *MyReader) Read() int {
return m.a + m.b
}
func DoJob(r Reader) {
fmt.Printf("myReader is %d\n", r.Read())
}
func main() {
myReader := &MyReader{2, 5}
DoJob(myReader)
}
运行结果:
bash
myReader is 7
若函数的形参为空接口,则实参可以为任意类型,因为空接口没有定义任何方法,任意类型都是空接口的一种实现。
go
package main
import "fmt"
func DoJob(value interface{}) {
fmt.Printf("value is %v\n", value)
}
func main() {
val := 10
DoJob(val)
}
运行结果:
bash
value is 10
接口嵌套
接口嵌套就是一个接口中包含了其他接口,如果要实现外部接口,则需要实现内部嵌套的接口对应的所有方法。
看具体例子:
go
package main
import "fmt"
type A interface {
run1()
}
type B interface {
run2()
}
// 定义嵌套接口C
type C interface {
A
B
run3()
}
type Runner struct{}
// 实现嵌套接口A的方法
func (r Runner) run1() {
fmt.Println("run1!!!!")
}
// 实现嵌套接口B的方法
func (r Runner) run2() {
fmt.Println("run2!!!!")
}
func (r Runner) run3() {
fmt.Println("run3!!!!")
}
func main() {
var runner C
runner = new(Runner) // runner实现了C接口的所有方法
runner.run1()
runner.run2()
runner.run3()
}
运行结果:
bash
run1!!!!
run2!!!!
run3!!!!
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!