go学习之接口知识

文章目录

接口

1.接口案例代码展示

模拟通过支持usb接口的手机和相机开始工作

go 复制代码
package main
import (
	"fmt"
)

//声明定义一个接口
type Usb interface{
	Start()
	Stop()
}

type Phone struct {

}

type Camera struct {

}
//让phone实现usb接口的方法
func (p Phone) Start(){
	fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop(){
	fmt.Println("手机停止工作。。。")
}

func (c Camera) Start(){
	fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop(){
	fmt.Println("相机停止工作。。。")
}
//计算机
type Computer struct{

}

//编写一个方法Working方法,接收一个Usb接口类型变量
//只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working (usb Usb){
	//通过usb接口变量来调用Start和Stop方法
     usb.Start()
	 usb.Stop()
}
func main(){

	//测试
	//先创建结构体变量
	computer :=Computer{}
	phone :=Phone{}
	camera := Camera{}

	//关键点
	computer.Working(phone)
	computer.Working(camera)


}

2.基本介绍

interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量,到某个自定义类型(比如结构体Phone)要使用的时候,再根据具体情况把这些方法写出来

3.基本语法

go 复制代码
type 接口名 interface{
  method1(参数列表)返回值列表
  method2(参数列表)返回值列表
}

实现接口的方法
func (t 自定义类型)method1(参数列表)返回值列表{
 //方法实现
}
func (t 自定义类型)method2(参数列表)返回值列表{
 //方法实现
}

小结说明:

1)接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想。

2)Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang中没有implement这样的关键字

解释:假如有一个不同名字的接口,但是方法和原接口的方法一样,那么实现的方法也是实现了这个接口,golang中接口的实现不是基于名字而是基于方法。可以同时实现两个接口。

go 复制代码
type Usb interface{
    Start()
	Stop()
}
type Usb2 interface{
	Start()
	Stop()
}

4.应用场景介绍

对初学者讲,理解接口不算太难,难的是不知道什么时候使用接口,下面几个例子来解释

1)现在美国要制造轰炸机,武装直升机,专家只需要把飞机需要的功能/规格定下来即可,然后让别人具体实现即可

2)就是做一个项目,在接口中定义规范让其他人去实现所定的规范

5.注意事项和细节

1)接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)

go 复制代码
package main
import (
	"fmt"
)

type AInterface interface {
	Say()

}

type Stu struct {
	Name string
}
func (stu Stu)Say(){
	fmt.Println("Stu Say()")
}
func main(){
   var stu Stu  //结构体变量,实现了Say() 实现了AInterface这个接口
   var a AInterface =stu
   a.Say() //Stu Say()
}

2)接口中所有的方法都没有方法体,即都是没有实现的方法

3)在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口

4)一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型

5)只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型(面试会问)

go 复制代码
type integer int
type AInterface interface {
	Say()
}

func (i integer)Say() {
	fmt.Println("integer Say i = ",i)
}
func main(){
   var i integer = 10
   var b AInterface = i
   b.Say() //integer Say i =  10
}

6)一个自定义类型可以实现多个接口

go 复制代码
package main
import (
	"fmt"
)

type AInterface interface {
	Say()

}

type Stu struct {
	Name string
}
func (stu Stu)Say(){
	fmt.Println("Stu Say()")
}

type integer int

func (i integer)Say() {
	fmt.Println("integer Say i = ",i)
}
type BInterface interface{
	Hello()
}
type Monster struct{

}
func (m Monster) Hello(){
	fmt.Println("Monster Hello()~~")
}
func (m Monster) Say(){
	fmt.Println("Monster Say()~~")
}
//此时刻Monster就实现了AInterface和BInterface

func main(){
   var stu Stu  //结构体变量,实现了Say() 实现了AInterface这个接口
   var a AInterface =stu
   a.Say() //Stu Say()
   var i integer = 10
   var b AInterface = i
   b.Say() //integer Say i =  10
//验证monster去实现两个接口
   var monster Monster
   var a2 AInterface = monster //Monster Say()~~
   var b2 BInterface = monster //Monster Hello()~~
   a2.Say()
   b2.Hello()
}

7)Golang接口中不能有任何变量

go 复制代码
例如,以下写法就是错误的
type AInteger interface {
Name string//这是错误的,不能这样用
test()
}

8)一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现

go 复制代码
package main
import (
	"fmt"
)
type BInterface interface {
	test01()
}
type CInterface interface {
	test02()
}
type AInterface interface {
	BInterface
	CInterface
	test03()
}
//如果需要实现AIerface,就需要把BInterface和CInterface的方法都实现
type Stu struct{

}
//把所要实现的接口所有的方法都实现一下
func (stu Stu) test01(){
  fmt.Println("这是Test01")
}
func (stu Stu) test02(){
	fmt.Println("这是Test02")
}
func (stu Stu) test03(){
	fmt.Println("这是Test03")
}

func main(){
    //实践
	var stu Stu
	var a AInterface = stu
	a.test01()
	a.test02()
	a.test03()
}

9)interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么就会输出nil

10)空接口interface{}没有任何方法,所以所有类型都实现了空接口

go 复制代码
type T interface {

}
func main(){
    //实践
	var stu Stu
	var a AInterface = stu
	a.test01()
	a.test02()
	a.test03()
	var t T =stu
	fmt.Println(t)
	var t2 interface{} = stu
	var num1 float64 = 8.8
	t2 = num1
	fmt.Println(t2,test02)
}

查看下列代码看看是否有错

go 复制代码
type AInterface interface{
   Test01()
   Test02()
}
type BInterface interface{
   Test01()
   Test03()
}
type CInterface interface{
   AInterface
   BInterface
}
func main(){

}
//这里编译错误,因为CInterface有两个test01().编译器不通过

来看第二个代码

go 复制代码
type Usb interface{
   Say()
}
type Stu struct {

}
func (this *Stu) Say() {
fmt,Println("Say()")
}

func main() {
var stu Stu = Stu{}
//var u Usb =stu //会报错,因为stu没有实现Say()方法
var u Usb = &stu //改正之后,加个地址就可以了
u.Say()
fmt.Println("here",u)
}

6.接口编程经典案例

实现对hero结构体切片的排序:sort.Sort(data Interface)

go 复制代码
package main
import (
	"fmt"
	"sort"
	"math/rand"
)

//1.声明Hero结构体
type Hero struct {
	Name string
	Age int
}

//2.声明一个Hero的切片类型
type HeroSlice []Hero 

//3.实现Interface 接口
func (hs HeroSlice) Len() int {
	return len(hs )
}
//Less方法就是决定你是用什么标准进行排序
//1.按Hero的年龄从小到大进行排序!!
func (hs HeroSlice) Less(i,j int) bool {
	return hs[i].Age > hs[j].Age
	//修改成对姓名排序
    // return hs[i].Name > hs[j].Name
}

func (hs HeroSlice) Swap(i,j int) {
	// temp := hs[i]
	// hs[i] =hs[j]
	// hs[j] = temp
	//简洁的交换:下面一句话等价于上面三句话
	hs[i],hs[j] = hs[j],hs[i]
}

//声明一个Student结构体
//1.声明Student结构体
type Student struct {
	Name string
	Age int
	Score int
}
//然后将上面那三个方法复制到下面

//将student按成绩从大到小进行排序

//声明一个Stu切片类型
type StuSlice []Student
//3.实现Interface 接口
func (stu StuSlice) Len() int {
	return len(stu)
}
//Less方法就是决定你是用什么标准进行排序
//1.按Hero的年龄从小到大进行排序!!
func (stu StuSlice) Less(i,j int) bool {
	//按成绩进行排序
	return stu[i].Score > stu[j].Score
}

func (stu StuSlice) Swap(i,j int) {
	stu[i],stu[j] = stu[j],stu[i]
}



func main(){

	//先定义一个数组/切片
	var intSlice = []int{0,-1,10,7,90}
    //要求对intSlice切片进行排序
	//1.冒泡排序...
	//2.可以使用系统提供的方法
    sort.Ints(intSlice)
	fmt.Println(intSlice)//[-1 0 7 10 90]
	//请对结构体进行排序
	//1.冒泡排序
	//2.系统提供的方法

	//测试我们是否可以对结构体进行排序
	var heroes HeroSlice
	for i :=0;i < 10; i++{
		hero :=Hero {
			Name : fmt.Sprintf("英雄~%d",rand.Intn(100)),
			Age : rand.Intn(100),
		}
		//将hero append到heros切片
		heroes = append(heroes,hero)
	}
	//看看排序前的顺序
	for _, v := range heroes {
		fmt.Println(v)
	}
	fmt.Println( )
    //调用sort.Sort()
    sort.Sort(heroes)
    //看看排序后的顺序
	for _, v := range heroes {
		fmt.Println(v)
	}

	//这个接口的妙处就是将这个接口的三个方法实现然后只需要将结合挂钩梯放入到
	//sort方法中去就可以
	fmt.Println()

	var studentsl StuSlice
	for i :=0;i < 10; i++{
		stus :=Student {
			Name : fmt.Sprintf("学生~%d",rand.Intn(100)),
			Age : rand.Intn(100),
			Score : rand.Intn(100),
		}
		//将hero append到heros切片
		studentsl = append(studentsl,stus)
	}
	//看看排序前的顺序
	for _, v := range studentsl {
		fmt.Println(v)
	}
	fmt.Println( )
    //调用sort.Sort()
    sort.Sort(studentsl)
    //看看排序后的顺序
	for _, v := range studentsl {
		fmt.Println(v)
	}
}

7.接口与继承之间的比较

1)大家可能对实现接口和继承比较迷茫了。那么问题来了,那么他们究竟有什么区别呢?

例如有一个猴子会爬树,然而他想学鸟儿飞翔,鱼儿游泳

go 复制代码
package main
import (
	"fmt"
)
//,Monkey结构体
type Monkey struct {
	Name string
}

//声明接口
type BirdAble interface {
	Flying()
}

//声明接口
type FishAble interface {
	Swimming()
}
func (this *Monkey) climbing() {
	fmt.Println(this.Name,"生来会爬树")
}

//创建LittleMonkey结构体
type LittleMonkey struct {
	Monkey //继承
}

//让littleMonkey实现BirdAble的Flying()方法
func (this *LittleMonkey) Flying(){
	fmt.Println(this.Name,"通过学习会飞翔")
}
//littleMonkey实现FishAble的Swimming()方法
func (this *LittleMonkey) Swimming(){
	fmt.Println(this.Name,"通过学习会游泳")
}
func main(){
    
	//创建LittleMonkey实例
	monkey := LittleMonkey{
		Monkey {
			Name : "悟空",
		},
	}
	monkey.climbing()
	monkey.Flying()
	monkey.Swimming()
  
}

对上面代码的小结

  1. 当A结构体继承了另外B结构体,那么A结构体就自动继承了B结构体的字段和方法,并且可以直接使用
  2. 当A结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充。

实现接口可以看作是对继承的一种补充

  • 接口和继承解决的问题是不同的

继承的价值 主要在于:解决代码的复用性可维护性

接口的价值主要在于:设计。设计好各种规范(方法),让其他自定义类型去实现这些方法

  • 接口比继承更加灵活 Person Student BirdAble LittleMonkey

  • 接口比继承更加灵活。继承是满足 is --a的关系。则接口只需满足like --a 的关系

  • 接口在一定程度上实现代码解耦

8.面向对象编程--多态

1)基本介绍

变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态

2)快速入门

在前面的Usb接口案例:Usb usb 既可以接收手机变量,又可以接收相机变量,就体现了Usb接口多态特性。

go 复制代码
//编写一个方法Working方法,接收一个Usb接口类型变量
//只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working (usb Usb){//usb变量会根据传入的实参,来判断到底是Phone还是Camera
	//通过usb接口变量来调用Start和Stop方法
     usb.Start()
	 usb.Stop()
}
func main(){

	//测试
	//先创建结构体变量
	computer :=Computer{}
	phone :=Phone{}
	camera := Camera{}

	//关键点
	computer.Working(phone)//传入phon就是phon执行这个方法
	computer.Working(camera)

}
3)接口体现多态的两种形式

多态参数

在前面的Usb接口案例,Usb usb,既可以接收手机变量,又可以接收相机变量,就体现了Usb的接口多态

多态数组

演示一个案例给Usb数组,存放phone结构体和Camera结构体变量

```go
package main
import (
	"fmt"
)

//声明定义一个接口
type Usb interface{
	Start()
	Stop()
}

type Phone struct {
    name string
}

type Camera struct {
	name string
}
//让phone实现usb接口的方法
func (p Phone) Start(){
	fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop(){
	fmt.Println("手机停止工作。。。")
}

func (c Camera) Start(){
	fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop(){
	fmt.Println("相机停止工作。。。")
}
//计算机
type Computer struct{

}

//编写一个方法Working方法,接收一个Usb接口类型变量
//只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working (usb Usb){
	//通过usb接口变量来调用Start和Stop方法
     usb.Start()
	 usb.Stop()
}
func main(){
   //定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
   //这里就体现出多态数组
   var usbArr  [3]Usb
   usbArr[0] = Phone{"iphone"}
   usbArr[1] = Phone{"小米"}
   usbArr[2] = Camera{"佳能"}
   fmt.Println(usbArr) //[{iphone} {小米} {佳能}]
	
}
```

9.类型断言

1)先看一个需求

代码

go 复制代码
type Point struct{
     x int
     y int
}
func main() {
    var a interface{}
    var point Point = Pooint{1,2}
    a = point //OK
    //如何将A赋给一个Point变量?
    var b Point
    b = a /可以吗? ==》erro
    fmt.Println(b)
}
//改进之后
package main
import (
	"fmt"
)
type Point struct{
	x int
	y int
}
func main() {
   var a interface{}
   var point Point = Point{1,2}
   a = point //OK
   //如何将A赋给一个Point变量?
   var b Point
   //b = a //可以吗? ==》erro
   b = a.(Point) //解决办法,类型断言
    //b = a.(Point)就是类型断言,表示判断a是否指向Poin类型的变量,如果是就转成了Point类型并赋给b变量,否则报错
   fmt.Println(b) //1 2
}

有一个具体的需要,引出了类型断言

2)基本介绍

类型断,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,具体的如下

go 复制代码
//float32可以是其他类型,比如Point
var t float32
var x interface{}
x = t //OK
y := x.(float32)//转成float32



//float32可以死其他类型,比如Point
var t float32 
var x interface{}
x = t
//转成float,待检查的
y,ok :=a.(float32)
if ok == true {
fmt.Println("convert success")
}else{
fmt.Println("convert fall")
}
go 复制代码
/类型断言的其他案例
   var x interface{}
   var b2 float32 = 1.1
   x = b2 //空接口可以接收任意类型
   //x = >float32 【使用类型断言】
   y := x.(float32)
   fmt.Printf("y的类型是%T 值是%v",y,y) //y的类型是float32 值是1.1

对上面的代码的说明

在进行类型断言时,如果类型不匹配,就会报panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型

如何在进行断言时带上检测机制,如果成功就ok否则也不要报panic

go 复制代码
//类型断言(带检测)
var x interface{}
var b2 float32 = 1.1
x = b2 //空接口可以接收任意类型
//x = >float32 【使用类型断言】
//y,ok := x.(float64)
//简写

if  y,ok := x.(float64);ok {
	fmt.Println("转换成功了")
    fmt.Printf("y的类型是%T 值是%v",y,y) //y的类型是float32 值是1.1
}else{
	fmt.Println("转换失败")
}
fmt.Println("检测失败了无妨,代码不要停")

}
3)类型断言的最佳实践1

在前面的Usb接口案例做改进

给Phone结构体增加一个特有的方法call(),当Usb接口接收的是Phone变量时,还需要调用call方法。

go 复制代码
package main
import (
	"fmt"
)

//声明定义一个接口
type Usb interface{
	Start()
	Stop()
}

type Phone struct {
    name string
}

type Camera struct {
	name string
}
//让phone实现usb接口的方法
func (p Phone) Start(){
	fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop(){
	fmt.Println("手机停止工作。。。")
}

func (p Phone) Call(){
	fmt.Println("手机开始打电话。。。")
}


func (c Camera) Start(){
	fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop(){
	fmt.Println("相机停止工作。。。")
}
//计算机
type Computer struct{

}

//编写一个方法Working方法,接收一个Usb接口类型变量
//只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working (usb Usb){
	//通过usb接口变量来调用Start和Stop方法
     usb.Start()
	 //如果usb是指向Phone结构体变量,则还需要调用Call()方法
	 //类型断言
	 if phone, ok := usb.(Phone); ok {
		phone.Call()
	 }//否则断言失败还是继续执行下列方法
	 usb.Stop()
}

func main(){
   //定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
   //这里就体现出多态数组
   var usbArr  [3]Usb
   usbArr[0] = Phone{"iphone"}
   usbArr[1] = Phone{"小米"}
   usbArr[2] = Camera{"佳能"}
   //fmt.Println(usbArr) //[{iphone} {小米} {佳能}]
   //遍历
   var computer Computer
   for _,v := range usbArr{
          computer.Working(v)
		  fmt.Println()
   }
	
}
执行结果如下:
手机开始工作。。。
手机开始打电话。。。
手机停止工作。。。

手机开始工作。。。
手机开始打电话。。。
手机停止工作。。。

相机开始工作。。。
相机停止工作。。。
4)类型断言实践2

写一个函数,循环判断传入参数的类型

go 复制代码
package main
import (
	"fmt"
)
//编写一个函数,可以判断输入的参数是什么类型
func TypeJudge(items... interface{}){
	for index, x := range items {
        switch x.(type) {
			case bool : 
				fmt.Printf("第%v个参数是 bool 类型,值是%v\n",index+1,x)
			case float32 : 
				fmt.Printf("第%v个参数是 float32 类型,值是%v\n",index+1,x)
			case float64 : 
				fmt.Printf("第%v个参数是 float64 类型,值是%v\n",index+1,x)	
			case int, int32, int64 : 
				fmt.Printf("第%v个参数是 整数 类型,值是%v\n",index+1,x)		  	  
			case string : 
				fmt.Printf("第%v个参数是 string 类型,值是%v\n",index+1,x)
			default :
			    fmt.Printf("第%v个参数是 类型 不确定,值是%v\n",index+1,x)	
		 }
	}
	
}
func main(){
	var n1 float32 = 1.1
	var n2 float64 = 2.3
	var n3 int32 = 30
	var name = "tom"
	address := "北京"
	n4 :=300
    
	TypeJudge(n1,n2,n3,name,address,n4)

}
//运行结果如下
第1个参数是 float32 类型,值是1.1
第2个参数是 float64 类型,值是2.3
第3个参数是 整数 类型,值是30
第4个参数是 string 类型,值是tom
第5个参数是 string 类型,值是北京
第6个参数是 整数 类型,值是300

5)类型断言的最佳实践3

在前面代码的基础上,增加判断Student类型和*Student类型

go 复制代码
package main
import (
	"fmt"
)

//定义Student类型
type Student struct {

}
//编写一个函数,可以判断输入的参数是什么类型
func TypeJudge(items... interface{}){
	for index, x := range items {
        switch x.(type) {
			case bool : 
				fmt.Printf("第%v个参数是 bool 类型,值是%v\n",index+1,x)
			case float32 : 
				fmt.Printf("第%v个参数是 float32 类型,值是%v\n",index+1,x)
			case float64 : 
				fmt.Printf("第%v个参数是 float64 类型,值是%v\n",index+1,x)	
			case int, int32, int64 : 
				fmt.Printf("第%v个参数是 整数 类型,值是%v\n",index+1,x)		  	  
			case Student : 
				fmt.Printf("第%v个参数是 Student 类型,值是%v\n",index+1,x)
			case *Student : 
				fmt.Printf("第%v个参数是 *Student 类型,值是%v\n",index+1,x)
			case string : 
				fmt.Printf("第%v个参数是 string 类型,值是%v\n",index+1,x)
						
			default :
			    fmt.Printf("第%v个参数是 类型 不确定,值是%v\n",index+1,x)	
		 }
	}
	
}


func main(){
	var n1 float32 = 1.1
	var n2 float64 = 2.3
	var n3 int32 = 30
	var name = "tom"
	address := "北京"
	n4 :=300

	stu1 := Student{}
	stu2 := &Student{}
    
	TypeJudge(n1,n2,n3,name,address,n4,stu1,stu2)

}

数是什么类型

func TypeJudge(items... interface{}){

for index, x := range items {

switch x.(type) {

case bool :

fmt.Printf("第%v个参数是 bool 类型,值是%v\n",index+1,x)

case float32 :

fmt.Printf("第%v个参数是 float32 类型,值是%v\n",index+1,x)

case float64 :

fmt.Printf("第%v个参数是 float64 类型,值是%v\n",index+1,x)

case int, int32, int64 :

fmt.Printf("第%v个参数是 整数 类型,值是%v\n",index+1,x)

case Student :

fmt.Printf("第%v个参数是 Student 类型,值是%v\n",index+1,x)

case *Student :

fmt.Printf("第%v个参数是 *Student 类型,值是%v\n",index+1,x)

case string :

fmt.Printf("第%v个参数是 string 类型,值是%v\n",index+1,x)

		default :
		    fmt.Printf("第%v个参数是 类型 不确定,值是%v\n",index+1,x)	
	 }
}

}

func main(){

var n1 float32 = 1.1

var n2 float64 = 2.3

var n3 int32 = 30

var name = "tom"

address := "北京"

n4 :=300

stu1 := Student{}
stu2 := &Student{}

TypeJudge(n1,n2,n3,name,address,n4,stu1,stu2)

}

复制代码
相关推荐
tyler_download39 分钟前
golang 实现比特币内核:实现基于椭圆曲线的数字签名和验证
开发语言·数据库·golang
hlsd#40 分钟前
go mod 依赖管理
开发语言·后端·golang
杜杜的man44 分钟前
【go从零单排】迭代器(Iterators)
开发语言·算法·golang
亦世凡华、44 分钟前
【启程Golang之旅】从零开始构建可扩展的微服务架构
开发语言·经验分享·后端·golang
jndingxin1 小时前
OpenCV相机标定与3D重建(1)概述
数码相机·opencv·3d
怀旧6661 小时前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
infiniteWei2 小时前
【Lucene】原理学习路线
学习·搜索引擎·全文检索·lucene
follycat2 小时前
[极客大挑战 2019]PHP 1
开发语言·学习·网络安全·php
并不会6 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
龙鸣丿6 小时前
Linux基础学习笔记
linux·笔记·学习