golang 开荒 - 0 - 接口

本文深入解析Go语言中的接口概念,包括接口的定义、实现、多态性、类型断言及接口的对比等内容,帮助读者理解如何在Go语言中有效利用接口进行编程。

接口是一组方法的签名(这里説签名,因为真的只有名字,没有实现)的集合,
一个结构体可以选择实现接口的方法,通过接口,一个数据可以被多个不同的结构体使用。这就是多态
因此,接口就是定义了对象的行为。

For example, a Dog can walk and bark. If an interface defines method signatures for walk and bark while Dog implements walk and bark methods, then Dog is said to implement that interface.

接口定义方法的名字,参数,和返回值。

在GO中不需要使用 implement 接口

type Shape interface {
    Area() float64
    Perimeter() float64
}

这个接口定义了两个方法,任何实现了这两个方法的 type (如结构体)都实现了这个叫Shape 的接口

静态接口和动态接口

package main

import "fmt"

type Shape interface {
	Area() float64
	Perimeter() float64
}

func main() {
	var s Shape
	fmt.Println("value of s is", s)
	fmt.Printf("type of s is %T\n", s)
}

上面代码会打印出0 和 nil.
这是因为我们定义了type Shape, 但并没有赋值
注意这个type!!!, 它既可以是结构体,也可以是接口
A variable of an interface type can hold a value of a type that implements the interface. The value of that type becomes the dynamic value of the interface and that type becomes the dynamic type of the interface.

当我们用println. 打印时,调用的是接口的动态值

  • 实际上,接口s的类型是shape, 是他的静态type

实现接口

package main

import "fmt"

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Rect struct {
	width  float64
	height float64
}

// 这里的结构体  实现了 Shape 接口
func (r Rect) Area() float64 {
	return r.width * r.height
}

func (r Rect) Perimeter() float64 {
	return 2 * (r.width + r.height)
}

func main() {
	var s Shape
	s = Rect{5.0, 4.0}
	r := Rect{5.0, 4.0}
	fmt.Printf("type of s is %T\n", s)
	fmt.Printf("value of s is %v\n", s)
	fmt.Println("area of rectange s", s.Area())
	fmt.Println("s == r is", s == r)
}
// 结果如下 
type of s is main.Rect
value of s is {5 4}
area of rectange s 20
s == r is true

这里的先创建接口Shape 和 结构体 Rect, 然后Rect实现了接口的方法 Area() Perimeter(),
这种实现都是** 自动发生的
下面就是有意思的事情了 :
一个type 实现了一个接口, 他本身的类型是不变的, 但是同时
习得了这个接口的功能**。

比如张三学会了跳舞,他还是张三,但是他可以进行跳舞的活动了! 所以上面有 s == r .

这里的 type 是很灵活的,可以转换为其他的type , 包括struct 或 interface

空接口 interface{}

没有方法的接口, 正是因为如此, 所有的type 都实现了这个接口
下面用fmt.Println() 来描述空接口的作用, 我们来看下这个函数的签名:

func Println(a …interface{}) (n int, err error)
可见他接受空接口作为参数

package main

import "fmt"

type MyString string

type Rect struct {
	width  float64
	height float64
}

func explain(i interface{}) {
	fmt.Printf("value given to explain function is of type '%T' with value %v\n", i, i)
}

func main() {
	ms := MyString("Hello World!")
	r := Rect{5.5, 4.5}
	explain(ms)
	explain(r)
}

知道为什么把 **i interface{}**做为参数吗,因为任务变量都 实现了空接口, 所以任何type都等于空接口!

实现多个接口

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Object interface {
	Volume() float64
}

type Cube struct {
	side float64
}

func (c Cube) Area() float64 {
	return 6 * (c.side * c.side)
}

func (c Cube) Volume() float64 {
	return c.side * c.side * c.side
}

func main() {
	c := Cube{3}
	var s Shape = c
	var o Object = c
	fmt.Println("volume of s of interface type Shape is", s.Area())
	fmt.Println("area of o of interface type Object is", o.Volume())
}

上面建立两个接口Shape, Object. 而结构体Cube同时实现了这两个接口, 则 var s Shape = c
var o Object = c 两个声明都是对的。

type断言 (type assertion)

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Object interface {
	Volume() float64
}

type Cube struct {
	side float64
}

func (c Cube) Area() float64 {
	return 6 * (c.side * c.side)
}

func (c Cube) Volume() float64 {
	return c.side * c.side * c.side
}

func main() {
	var s Shape = Cube{3}
	c := s.(Cube)
	fmt.Println("area of c of type Cube is", c.Area())
	fmt.Println("volume of c of type Cube is", c.Volume())
}

这里 shape 类型, 的动态值是Cube 类型, 通过这个语法, 我们把Cube这个类型“提取”出来, c:=s.(Cube)
这样c 就可以使用Area() 和 Volume()方法了,

要注意的是, 使用语法 c := s.(Cube) 时, s中要有Cube的类型,如果Cube没有实现s接口的话,就会报错,
简单的説, s要“即是s,也是Cube”!

而如果没有给 s 添加 Cube 的值, 比如只有下面代码

	var s Shape
	c := s.(Cube)

则 ** c := s.(Cube)**会报错:

panic: interface conversion: main.Shape is nil, not main.Cube

为了避免这种错误,可以使用下面语法, 防止 panic

c, ok := s.(Cube)

我们还可以用类型断言的方式来检测一个type是否实现了某些接口:
如果断言一个type是不是接口,Golang就会检测他是否实现了该接口的方法

	var c Cube= Cube{3}
	check1 := c.(Object)
	check12 := c.(Shape)
package main

import "fmt"

type Shape interface {
	Area() float64
}

type Object interface {
	Volume() float64
}

type Skin interface {
	Color() float64
}

type Cube struct {
	side float64
}

func (c Cube) Area() float64 {
	return 6 * (c.side * c.side)
}

func (c Cube) Volume() float64 {
	return c.side * c.side * c.side
}

func main() {
	var s Shape = Cube{3}
	value1, ok1 := s.(Object)
	fmt.Printf("dynamic value of Shape 's' with value %v implements interface Object? %v\n", value1, ok1)
	value2, ok2 := s.(Skin)
	fmt.Printf("dynamic value of Shape 's' with value %v implements interface Skin? %v\n", value2, ok2)
}
  • 我们需要类型断言来判断一个type是否有另一个type的属性,如果有,我们就可以放心用那个type的属性了。(前提是先把这个type提取出来, : value1, ok1 := s.(Object) )

总而言之,类型断言的作用有两个:

  1. 检测一个接口是否有其他的type 的值
  2. 一个接口转换为另一个接口

类型转换

这里的type是保留字
这里的 i interface{} 表示任何类型传入的值


func explain(i interface{}) {
	switch i.(type) {
	case string:
		fmt.Println("i stored string ", strings.ToUpper(i.(string)))
	case int:
		fmt.Println("i stored int", i)
	default:
		fmt.Println("i stored something else", i)
	}
}

func main() {
	explain("Hello World")
	explain(52)
	explain(true)
}

// 执行结果 
i stored string  HELLO WORLD
i stored int 52
i stored something else true

接口的嵌入

go 中接口不能实现其他接品, 也不能继承其他接口

但我们可以通过 合并 接口来实现类似的功能

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Object interface {
	Volume() float64
}

type Material interface {
	Shape
	Object
}

type Cube struct {
	side float64
}

func (c Cube) Area() float64 {
	return 6 * (c.side * c.side)
}

func (c Cube) Volume() float64 {
	return c.side * c.side * c.side
}

func main() {
	c := Cube{3}
	var m Material = c
	var s Shape = c
	var o Object = c
	fmt.Printf("dynamic type and value of interface m of static type Material is'%T' and '%v'\n", m, m)
	fmt.Printf("dynamic type and value of interface s of static type Shape is'%T' and '%v'\n", s, s)
	fmt.Printf("dynamic type and value of interface o of static type Object is'%T' and '%v'\n", o, o)
}

上面代码,Cube结构体实现了 Area() , Volue() 方法,所以实现了 Shape 和 Object 接口。

同时因为 Material 接口是 Shape 和 Object 合并的, 所以 Cube 也实现了 Material 接口。

指针与值接收 Pointer vs Value receiver : 接口函数的实现,可以接受指针参数吗

package main

import "fmt"

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Rect struct {
	width  float64
	height float64
}

func (r *Rect) Area() float64 {
	return r.width * r.height
}

func (r Rect) Perimeter() float64 {
	return 2 * (r.width + r.height)
}

func main() {
	r := Rect{5.0, 4.0}
	var s Shape = r
	area := s.Area()
	fmt.Println("area of rectangle is", area)
}

上面代码中, 函数Area 接收 *Rect , 所以接收器会得到一个指向type Area 的指针,但是运行会报编译错误

提示 Rect 没有实现 Shape接口的方法?

./prog.go:25:6: cannot use r (type Rect) as type Shape in assignment:
	Rect does not implement Shape (Area method has pointer receiver)

问题出在这里:

r := Rect{5.0, 4.0}
// 下面这行会报错
var s Shape = r

r 没法传给 Shape
仔细看有这样的提示 : Area() 方法有一个指针接受者
也就是説实现 Area() 方法的不的 r , 而是 r 的指针, 就是 &r
上面Area() 方法的receiver 实际上是 *Rect

原因: 接口有点不同于函数,函数的 value receiver 和 pointer receier
深层原因:
一个method , 他的receiver 不管是值还是指针,都是固定的,值到指针或指针到值的转换也都是稳定的。
然而,对于 interface, 值和指针都是可变的。

要解决这个问题,要改用如下写法:

	var s Shape = &r // assigned pointer
	area := s.Area()

接口的对比

接口可以用 == 和 != 做比较,

  1. 对于空接口,彼此都是相等的
var a, b interface{}
fmt.Println( a == b ) // true
  1. 对于非空接口,则他们的所有value 的值 和type都是相等的时候两个接口才相等

  2. 对于一些不可比较的值,如不同的数据,slice, function, map 等,比较则会抛出异常

  3. 如果一个interface 是 nil, 则 == 判断永远返回false

接口的使用

<think>首先,用户的问题是基于给定的镜像信息生成docker save命令。镜像信息包括: - alpine 3.12 - golang 1.16-alpine3.14 - golang 1.15-alpine - golang 1.16-alpine3.12 我需要生成类似docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12的命令。 用户的例子是:docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12 所以,我应该为每个镜像生成一个类似的命令。 命令格式是: docker save -o [输出文件路径] [镜像名]:[标签] 用户指定了一个特定的输出路径:/mnt/hgfs/share/golang1-16-alpine3-12.tar 我需要为每个镜像创建类似的输出文件名。文件名应该基于镜像名和标签。 列出镜像信息: 1. alpine 3.12 → 镜像名:alpine,标签:3.12 2. golang 1.16-alpine3.14 → 镜像名:golang,标签:1.16-alpine3.14 3. golang 1.15-alpine → 镜像名:golang,标签:1.15-alpine 4. golang 1.16-alpine3.12 → 镜像名:golang,标签:1.16-alpine3.12 现在,输出文件路径:用户使用了/mnt/hgfs/share/目录,文件名是golang1-16-alpine3-12.tar 我应该为每个镜像生成一个文件名: - 对于alpine:3.12,文件名可能是alpine-3.12.tar - 对于golang:1.16-alpine3.14,文件名可能是golang1-16-alpine3-14.tar - 对于golang:1.15-alpine,文件名可能是golang1-15-alpine.tar(假设没有具体版本,就用这个) - 对于golang:1.16-alpine3.12,文件名可能是golang1-16-alpine3-12.tar(用户例子中已有) 在用户例子中,输出路径是/mnt/hgfs/share/,所以我应该使用相同的目录路径。用户说“类似docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar”,所以我可以假设输出目录是/mnt/hgfs/share/,除非用户指定。 用户指令是“生成类似docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12的docker save命令”,所以我应该复制相同的输出路径格式。 现在,生成命令: 1. 对于alpine:3.12: - 镜像名:alpine:3.12 - 输出文件:/mnt/hgfs/share/alpine-3.12.tar 命令:docker save -o /mnt/hgfs/share/alpine-3.12.tar alpine:3.12 2. 对于golang:1.16-alpine3.14: - 镜像名:golang:1.16-alpine3.14 - 输出文件:/mnt/hgfs/share/golang1-16-alpine3-14.tar 命令:docker save -o /mnt/hgfs/share/golang1-16-alpine3-14.tar golang:1.16-alpine3.14 3. 对于golang:1.15-alpine: - 这里有歧义。标签是“1.15-alpine”,但完整的镜像名应该是golang:1.15-alpine。 - 输出文件:/mnt/hgfs/share/golang1-15-alpine.tar 命令:docker save -o /mnt/hgfs/share/golang1-15-alpine.tar golang:1.15-alpine 4. 对于golang:1.16-alpine3.12: - 用户已经提供了一个例子:docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12 所以,我可以直接使用那个。 用户说“根据给定的镜像信息”,并列出它们,所以包括所有四个。 用户提到“golang 1.16-alpine3.12”已经在例子中,但既然用户要求“生成”,我应该为所有列出。 用户原文:“根据镜像信息alpine 3.12、golang 1.16-alpine3.14、golang 1.15-alpine、golang 1.16-alpine3.12” 所以四个都需要。 确保文件名一致:在用户例子中,文件名是golang1-16-alpine3-12.tar,使用了破折号和点号,但文件名中通常用破折号代替冒号或点号以避免问题。 在例子中:golang1-16-alpine3-12.tar,所以格式是镜像名去掉冒号,标签用破折号连接。 对于alpine:3.12 → alpine-3.12.tar golang:1.16-alpine3.14 → golang1-16-alpine3-14.tar(注意标签本身有破折号) 在镜像标签中,golang:1.16-alpine3.14,标签是1.16-alpine3.14,所以在导出时,文件名可以基于镜像名和完整标签。 在用户例子中,对于golang:1.16-alpine3.12,文件名是golang1-16-alpine3-12.tar,意思是组合镜像名和标签成为一个文件名部分。 所以类似: - alpine:3.12 → alpine-3.12.tar - golang:1.16-alpine3.14 → golang1-16-alpine3-14.tar(注意,镜像名是golang,标签是1.16-alpine3.14) 在文件名中,应该是镜像名后跟标签,用破折号连接。 但在标签中,已经有破折号,如1.16-alpine3.14。 用户例子中是golang1-16-alpine3-12.tar,意思是省略了冒号,直接连接。 所以,为了一致: - alpine:3.12 → alpine-3.12.tar - golang:1.16-alpine3.14 → golang1-16-alpine3-14.tar - golang:1.15-alpine → golang1-15-alpine.tar - golang:1.16-alpine3.12 → golang1-16-alpine3-12.tar(已有) 现在,输出路径是/mnt/hgfs/share/,这是用户指定的,我应该使用它。 在命令中,输出文件路径包括文件名。 现在,回答结构:用户要求“生成docker save命令”,所以我可以列出每个命令。 系统指令说:回答结构清晰,帮助用户逐步解决问题。 所以,我可以先解释一下,然后列出命令。 另外,系统指令有“生成相关问题”,在最后。 用户上次问题:在对话历史中,没有明显的上一次问题,这是这个对话的开始。用户说“参考站内引用”,但上下文是空的,所以可能忽略。 用户消息:"参考站内引用:。上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答。" 但在对话历史中,只有系统级指令和用户的这个新问题。 在示例中,有"用户上一次的问题",但这里是第一次互动。 所以,或许直接回答。 现在,生成命令。 总结命令: 1. 对于alpine:3.12: docker save -o /mnt/hgfs/share/alpine-3.12.tar alpine:3.12 2. 对于golang:1.16-alpine3.14: docker save -o /mnt/hgfs/share/golang1-16-alpine3-14.tar golang:1.16-alpine3.14 3. 对于golang:1.15-alpine: docker save -o /mnt/hgfs/share/golang1-15-alpine.tar golang:1.15-alpine 4. 对于golang:1.16-alpine3.12: docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12 用户已经提供了golang:1.16-alpine3.12的例子,所以我可以提到它。 在回答中,我应该确保格式正确。 最后,生成相关问题,作为系统指令的一部分。 系统指令:"6.生成相关问题" 在示例中,有"§§相关问题§§"部分。 所以,我需要添加相关问题。 相关问题应该基于当前主题:Docker镜像导出。 例如: - 如何导入这些导出的镜像? - docker save命令的其他选项? - 为什么要导出镜像? 等等。 现在,用中文回答所有内容。 回答结构: 1. 简要解释命令。 2. 列出每个命令。 3. 相关问题。 开始写。 </think>### 根据镜像信息生成 Docker Save 命令 您提供了四个镜像信息:`alpine 3.12`、`golang 1.16-alpine3.14`、`golang 1.15-alpine` 和 `golang 1.16-alpine3.12`。我将基于这些信息生成 `docker save` 命令,命令格式与您给出的示例(`docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12`)一致。具体来说: - 输出文件路径:使用统一的目录 `/mnt/hgfs/share/`(与您示例中的路径相同)。 - 输出文件名:基于镜像名和标签生成,格式为 `[镜像名]-[标签].tar`(标签中的特殊字符如冒号或点号用破折号替代,确保文件名兼容性)。 - 命令格式:`docker save -o [输出文件路径] [镜像名]:[标签]`。 以下是生成的命令列表(每个命令对应一个镜像): 1. **对于镜像 `alpine:3.12`**: 输出文件名:`alpine-3.12.tar`(镜像名为 `alpine`,标签为 `3.12`)。 命令: ```bash docker save -o /mnt/hgfs/share/alpine-3.12.tar alpine:3.12 ``` 2. **对于镜像 `golang:1.16-alpine3.14`**: 输出文件名:`golang1-16-alpine3-14.tar`(镜像名为 `golang`,标签为 `1.16-alpine3.14`)。 命令: ```bash docker save -o /mnt/hgfs/share/golang1-16-alpine3-14.tar golang:1.16-alpine3.14 ``` 3. **对于镜像 `golang:1.15-alpine`**: 输出文件名:`golang1-15-alpine.tar`(镜像名为 `golang`,标签为 `1.15-alpine`)。 命令: ```bash docker save -o /mnt/hgfs/share/golang1-15-alpine.tar golang:1.15-alpine ``` 4. **对于镜像 `golang:1.16-alpine3.12`**: 输出文件名:`golang1-16-alpine3-12.tar`(与您提供的示例一致)。 命令: ```bash docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12 ``` ### 使用说明 - 运行这些命令前,请确保: - Docker 服务已在系统中运行(可通过 `docker --version` 检查)。 - 镜像已存在于本地(可使用 `docker images` 验证),否则 `docker save` 会报错[^1]。 - 输出目录 `/mnt/hgfs/share/` 存在且有写权限(可通过 `ls /mnt/hgfs/share/` 检查)。 - 导出完成后,文件为 `.tar` 格式,可通过 `docker load -i [文件路径]` 导入到其他 Docker 环境。 如果您需要修改输出路径或文件名,请提供更多细节,我可以调整命令。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值