Golang通脉之接口
Posted 羌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang通脉之接口相关的知识,希望对你有一定的参考价值。
接口(interface
)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
接口类型
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface
是一组函数或方法的集合,是duck-type programming
的一种体现。接口做的事情就像是定义一个协议(规则),不关心属性(数据),只关心行为(方法),请牢记接口(interface
)是一种类型。
接口与鸭子类型:
维基百科的定义:
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
翻译过来就是:如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。
Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过接口的方式完美支持鸭子类型。
而在静态语言如 Java, C++ 中,必须要显示地声明实现了某个接口之后,才能用在任何需要这个接口的地方。如果你在程序中调用某个数,却传入了一个根本就没有实现另一个的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。
动态语言和静态语言的差别在此就有所体现。静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到那一行代码才会报错。当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上,加大了工作量,也加长了代码量。动态语言则没有这些要求,可以让人更专注在业务上,代码也更短,写起来更快。
Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。
总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。
Go语言的多态性:
在Java语言中,多态是通过继承和重写来体现的,而Go中的多态性就是在接口的帮助下实现的。接口可以在Go中隐式地实现。如果类型为接口中声明的所有方法提供了定义,则该类型实现了这个接口。
任何定义了接口所有方法的类型都被称为隐式地实现了该接口。
类型接口的变量可以保存实现接口的任何值。接口的这个属性用于实现Go中的多态性。
什么是接口
简言之:
- 接口是一组方法签名
- 接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口
接口的定义
Go语言提倡面向接口编程。
每个接口由数个方法组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
- 接口名:使用
type
将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子:
type writer interface{
Write([]byte) error
}
当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。
实现接口的条件
一个类型只要实现了接口中的全部方法,那么就实现了这个接口。换句话说,接口就是一组需要实现的方法签名。
定义一个接口并实现它:
type IPhone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type MobilePhone struct {
}
func (mobilePhone MobilePhone) call() {
fmt.Println("I am mobile, I can call you!")
}
接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。
接口类型变量
为什么要实现接口呢?
接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,IPhone
类型的变量能够存储NokiaPhone
和MobilePhone
类型的变量。
func main() {
var x IPhone // 声明一个Sayer类型的变量x
a := NokiaPhone{} // 实例化一个NokiaPhone
b := MobilePhone{} // 实例化一个MobilePhone
x = a // 可以把NokiaPhone实例直接赋值给x
x.call() // I am Nokia, I can call you!
x = b // 可以把Phone实例直接赋值给x
x.call() // I am mobile, I can call you!
}
Tips: 观察下面的代码,体味此处_
的妙用
// 摘自gin框架routergroup.go
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter
值接收者和指针接收者实现接口的区别
使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?
定义一个方法,有值类型接收者和指针类型接收者两种。二者都可以调用方法,因为 Go 语言编译器自动做了转换,所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中,值类型接收者和指针类型接收者不一样。
type Stringer interface {
String() string
}
type person struct {
name string
age uint
addr address
}
type address struct {
province string
city string
}
func (p person) String() string{
return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}
func (addr address) String() string{
return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}
func printString(s fmt.Stringer){
fmt.Println(s.String())
}
func main(){
p := person{}
printString(p) //正常输出
printString(p.addr) //正常输出
printString(&p) //正常输出
}
把变量 p 的指针作为实参传给 printString 函数也是可以的,编译运行都正常。这就证明了以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口。
值接收者(p person
)实现了 Stringer
接口,那么类型 person
和它的指针类型*person
就都实现了 Stringer
接口。
再把接收者改成指针类型:
type Stringer interface {
String() string
}
type person struct {
name string
age uint
addr address
}
type address struct {
province string
city string
}
func (p *person) String() string{
return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}
func (addr address) String() string{
return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}
func printString(s fmt.Stringer){
fmt.Println(s.String())
}
func main(){
p := person{}
printString(p) //cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)
printString(&p) //正常输出
}
修改成指针类型接收者后会发现,提示错误:
cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)
意思就是类型 person
没有实现 Stringer
接口。这就证明了以指针类型接收者实现接口的时候,只有对应的指针类型才被认为实现了该接口。
总结:
当值类型作为接收者时,person
类型和*person
类型都实现了该接口。
当指针类型作为接收者时,只有*person
类型实现了该接口。
可以发现,实现接口的类型都有*person
,这也表明指针类型比较万能,不管哪一种接收者,它都能实现该接口。
类型与接口的关系
一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。分别定义Sayer接口和Mover接口:
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
dog既可以实现Sayer接口,也可以实现Mover接口。
type dog struct {
name string
}
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫\\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\\n", d.name)
}
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}
多个类型实现同一接口
Go语言中不同的类型还可以实现同一接口 首先义一个Mover
接口,它要求必须由一个move
方法。
// Mover 接口
type Mover interface {
move()
}
如狗可以动,汽车也可以动,实现这个关系:
type dog struct {
name string
}
type car struct {
brand string
}
// dog类型实现Mover接口
func (d dog) move() {
fmt.Printf("%s会跑\\n", d.name)
}
// car类型实现Mover接口
func (c car) move() {
fmt.Printf("%s速度70迈\\n", c.brand)
}
这个时候可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move
方法就可以了。
func main() {
var x Mover
var a = dog{name: "旺财"}
var b = car{brand: "保时捷"}
x = a
x.move()
x = b
x.move()
}
并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("脱水")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗衣服")
}
接口嵌套
接口与接口间可以通过嵌套创造出新的接口。
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
嵌套得到的接口的使用与普通接口一样。
空接口
空接口的定义
空接口是指没有定义任何方法签名的接口。由于任何类型都至少实现了0个方法,因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
type I interface{}
func main() {
var i I
i = 42 //这个时候i就是int类型
fmt.Printf("%v,%T\\n", i, i)
i = "hello" //这个时候i就是string类型
fmt.Printf("%v,%T\\n", i, i)
}
42,int
hello,string
空接口的应用
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\\n", a, a)
}
空接口作为map的值
使用空接口实现可以保存任意值的映射。
// 空接口作为map值
var person = make(map[string]interface{})
person["name"] = "张三"
person["age"] = 23
person["married"] = false
fmt.Println(person)
空接口对切片的影响
- 若将一个
array
或slice
赋值给空接口,这个空接口无法再进行切片 array
或slice
赋值给空接口的行为不是复制,而是类似指针效果,只不过无法再进行切片,但元素和原来的array
、slice
及其衍生的,都有关联
func main() {
s := []int{2, 3, 5, 7, 11, 13}
var e interface{}
e = s
f := s[0:3]
f[2] = 55
fmt.Printf("%T,%v\\n", s, s)
fmt.Printf("%T,%v\\n", e, e)
fmt.Printf("%T,%v\\n", f, f)
}
输出
[]int,[2 3 55 7 11 13]
[]int,[2 3 55 7 11 13]
[]int,[2 3 55]
若改为
func main() {
s := []int{2, 3, 5, 7, 11, 13}
var e interface{}
e = s
g := e[1:3]
fmt.Println(g)
}
报错
cannot slice e (type interface {})
空接口的赋值
空接口可以存储任意值,但不代表任意类型就可以存储空接口类型的值
从实现的角度看,任何类型的值都满足空接口。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
但要是把一个空接口类型的对象,再赋值给一个固定类型(比如 int
, string
等类型)的对象赋值,是会报错的。
func main() {
// 声明a变量, 类型int, 初始值为1
var a int = 1
// 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
var i interface{} = a
// 声明b变量, 尝试赋值i
var b int = i
}
这个报错,它就好比可以放进行礼箱的东西,肯定能放到集装箱里,但是反过来,能放到集装箱的东西就不一定能放到行礼箱了,在 Go 里就直接禁止了这种反向操作。
cannot use i (type interface {}) as type int in assignment: need type assertion
类型断言
空接口可以存储任意类型的值,那如何获取其存储的具体数据呢?
接口值
一个接口的值(简称接口值)是由一个具体类型
和具体类型的值
两部分组成的。这两部分分别称为接口的动态类型
和动态值
:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
请看下图分解:
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
// 安全类型断言
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )
//非安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )
示例代码:
func main() {
var i1 interface{} = new (Student)
s := i1.(Student) //不安全,如果断言失败,会直接panic
fmt.Println(s)
var i2 interface{} = new(Student)
s, ok := i2.(Student) //安全,断言失败,也不会panic,只是ok的值为false
if ok {
fmt.Println(s)
}
}
type Student struct {}
断言其实还有另一种形式,就是用在利用 switch
语句判断接口的类型。每一个case
会被顺序地考虑。当命中一个case
时,就会执行 case
中的语句,因此 case
语句的顺序是很重要的,因为很有可能会有多个 case
匹配的情况:
switch ins:=s.(type) {
case Triangle:
fmt.Println("三角形。。。",ins.a,ins.b,ins.c)
case Circle:
fmt.Println("圆形。。。。",ins.radius)
case int:
fmt.Println("整型数据。。")
}
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
任督二脉之进程管理
一、第二次课大纲
1.fork、vfork、clone
2.写时拷贝技术
3.Linux线程的实现本质
4.进程0和进程1
5.进程的睡眠和等待队列
6.孤儿进程的托孤,SUBREAPER
1.fork、vfork、Copy-on-Write例子
2.life-period例子,实验体会托孤
3.pthread_create例子,strace它
4.彻底看懂等待队列的案例
二、fork
1)fork创建一个进程。p1是一个task_struct,p2也是一个task_struct,调度器只通过task_struct来调度进程。在p1中fork后新进程p2,直接完全对拷给p2的task_struct,两者完全一样(当然pid不同),资源一样。只要有修改就不一样了,资源分裂。比如fs,fd等。chdir函数修改cwd。
2)但是内存资源mm不好分裂。采用写时拷贝技术(COW)
例子:
1 #include <sched.h> 2 3 #include <unistd.h> 4 5 #include <stdio.h> 6 7 #include <stdlib.h> 8 9 10 11 int data = 10; 12 13 14 15 int child_process() 16 17 { 18 19 printf("Child process %d, data %d ",getpid(),data); 20 21 data = 20; 22 23 printf("Child process %d, data %d ",getpid(),data); 24 25 _exit(0); 26 27 } 28 29 30 31 int main(int argc, char* argv[]) 32 33 { 34 35 int pid; 36 37 pid = fork(); 38 39 40 41 if(pid==0) { 42 43 child_process(); 44 45 } 46 47 else{ 48 49 sleep(1); 50 51 printf("Parent process %d, data %d ",getpid(), data); 52 53 exit(0); 54 55 } 56 57 }
运行结果:
下面是COW详细过程:fork后父子进程的页表完全一样,但是只有RD_ONLY,其中一个进程P2修改数据,那么就申请一个新的物理页面,把原页面数据拷贝到新的物理页面,再修改P2进程页表。
3)从上面可以看出,COW技术依赖于MMU(硬件),那么没有MMU的系统,比如uclinux,不能执行COW,没有fork函数,只有vfork。vfork会阻塞父进程(子进程调用_exit,exec之一才返回)。所以vfork后P1和P2的mm指针指向同一个地址。
vfork后P1和P2的mm指针指向同一个地址。
例子:
1 #include <sched.h> 2 3 #include <unistd.h> 4 5 #include <stdio.h> 6 7 #include <stdlib.h> 8 9 10 11 int data = 10; 12 13 14 15 int child_process() 16 17 { 18 19 printf("Child process %d, data %d ",getpid(),data); 20 21 data = 20; 22 23 printf("Child process %d, data %d ",getpid(),data); 24 25 _exit(0); 26 27 } 28 29 30 31 int main(int argc, char* argv[]) 32 33 { 34 35 int pid; 36 37 pid = vfork(); 38 39 40 41 if(pid==0) { 42 43 child_process(); 44 45 } 46 47 else{ 48 49 sleep(1); 50 51 printf("Parent process %d, data %d ",getpid(), data); 52 53 exit(0); 54 55 } 56 57 }
运行结果:
那么可以放大,把各种资源全部指向同一个地址,资源全部共享。也即pthread_create(其调用了clone函数,再共享其他资源),这就是轻量级进程,即线程。
4)也可以只共享部分资源,clone函数的参数指定。既不是进程也不是线程。
5)PID和TGID
以上fork,vfork,clone都会产生新的task_struct,有独立的pid。POSIX要求一个进程里面有多个线程,必须对外表现为一个整体,getpid时是同一个值。linux就增加一个TGID=P1。
例子:
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <stdio.h> 4 #include <linux/unistd.h> 5 #include <sys/syscall.h> 6 7 static pid_t gettid( void ) 8 { 9 10 return syscall(__NR_gettid); 11 12 } 13 14 static void *thread_fun(void *param) 15 { 16 17 printf("thread pid:%d, tid:%d pthread_self:%lu ", getpid(), gettid(),pthread_self()); 18 19 while(1); 20 21 return NULL; 22 } 23 24 int main(void) 25 { 26 pthread_t tid1, tid2; 27 int ret; 28 29 printf("thread pid:%d, tid:%d pthread_self:%lu ", getpid(), gettid(),pthread_self()); 30 /*getpid():TGID, gettid():内核空间的pid,pthread_self(): 用户空间thread库里的id*/ 31 32 33 ret = pthread_create(&tid1, NULL, thread_fun, NULL); 34 35 if (ret == -1) { 36 37 perror("cannot create new thread"); 38 39 return -1; 40 41 } 42 43 ret = pthread_create(&tid2, NULL, thread_fun, NULL); 44 if (ret == -1) { 45 perror("cannot create new thread"); 46 return -1; 47 } 48 49 if (pthread_join(tid1, NULL) != 0) { 50 perror("call pthread_join function fail"); 51 return -1; 52 } 53 54 if (pthread_join(tid2, NULL) != 0) { 55 perror("call pthread_join function fail"); 56 return -1; 57 } 58 59 return 0; 60 }
top命令看到的多线程进程是一个整体,top -H就是线程视角,也可以运行top后按H转换视角。
pstree:
进程视角:
线程视角:
三、subreaper和托孤
subreaper:通过系统调用设置自己为subreaper,pstree里面显示为init的为subreaper。
在进程退出后会在父子关系树向上找sbureaper,把子进程托孤给它,找不到sbureaper看托孤给init进程。
例子,托孤:先pstree观察父子进程在树形结构中的位置,然后把父进程杀掉,pstree观察子进程在树形结构中的位置变化。
1 #include <stdio.h> 2 #include <sys/wait.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 6 int main(void) 7 { 8 pid_t pid,wait_pid; 9 int status; 10 11 pid = fork(); 12 if (pid==-1) { 13 perror("Cannot create new process"); 14 exit(1); 15 } else if (pid==0) { 16 printf("child process id: %ld ", (long) getpid()); 17 pause(); 18 _exit(0); 19 } else { 20 printf("parent process id: %ld ", (long) getpid()); 21 wait_pid=waitpid(pid, &status, WUNTRACED | WCONTINUED); 22 if (wait_pid == -1) { 23 24 perror("cannot using waitpid function"); 25 exit(1); 26 27 } 28 29 if(WIFSIGNALED(status)) 30 printf("child process is killed by signal %d ", WTERMSIG(status)); 31 32 exit(0); 33 } 34 }
三、睡眠:深度睡眠VS浅度睡眠
睡眠的实现:关键--等待队列(类似设计模式中的观察者模式),等待资源的进程加入等待队列,资源到来后唤醒等待队列不会唤醒单个进程。看globalfifo代码:https://github.com/21cnbao/process-courses/blob/master/day2/globalfifo.c
四、进程0和1
1号进程的父进程pid=0。但是pstree里面看不到0号进程,实际上0号进程创建完init进程后就退化为idle进程,也就是其他进程都不用cpu的情况下才运行,也即调度优先级最低,进入睡眠状态WFI,省电。设计的好处:每个进入进入睡眠之前就查一下是不是最后一个进程,是的话就置为WFI低功耗,这样就耦合到了每个进程里面;idle的设计就没有耦合了。
1 [email protected]:~/vmware-tools-distrib$ cat /proc/1/status 2 Name: systemd 3 State: S (sleeping) 4 Tgid: 1 5 Ngid: 0 6 Pid: 1 7 PPid: 0 8 TracerPid: 0 9 Uid: 0 0 0 0 10 Gid: 0 0 0 0 11 FDSize: 256 12 Groups: 13 NStgid: 1 14 NSpid: 1 15 NSpgid: 1 16 NSsid: 1
五、问答
1、进程0是天上掉下来的,是从boot跑到linux。???????也是有task_struct的。
2、vfork阻塞父进程,子进程一般是创建后执行exec。
3、多线程的进程,/proc/tgid/task/pid。
4、MMU原理,数据结构
http://ytliu.info/blog/2014/11/24/shi-shang-zui-xiang-xi-de-kvm-mmu-pagejie-gou-he-yong-fa-jie-xi/
以上是关于Golang通脉之接口的主要内容,如果未能解决你的问题,请参考以下文章