【Go编程学习】结构体、方法、接口

结构体

Go 语言中没有“类”的概念,也不支持如继承这种面向对象的概念。但是Go 语言的结构体与“类”都是复合结构体,而且Go 语言中结构体的组合方式比面向对象具有更高的扩展性和灵活性。

结构体定义

/*
type typeName struct{
    FieldName FieldType
    FieldName FieldType
    FieldName FieldType
}
*/
type Person struct {
    
    
    Name string
    Age int
}

结构体中字段的类型可以是任何类型,包括函数类型,接口类型,甚至结构体类型本身。例如我们声明一个链表中的节点的结构体类型。

type ListNode struct{
    
    
    Val int
    Next *ListNode
}

在声明结构体时我们也可以不给字段指定名字,例如下面这样:

type Person struct {
    
    
	ID string
	int
}

没有被指定名字的字段被称为匿名字段

结构体操作

初始化时推荐使用带有FieldName的初始化方式,没有指定的字段默认初始化为类型的零值。不说明FieldName,按类型声明顺序逐个赋值的方式,一旦struct增加字段,整个初始化语句会报错:

s := new(Person)
a := Person{
    
    "Tom",21}//不推荐
b := Person{
    
    
    Name: "T",
    Age:12,
}

new函数会创建一个指向结构体类型的指针,创建过程中会自动为结构体分配内存,结构体中每个变量被赋予对应的零值。

声明完结构体之后可以直接按如下方式操作结构体。

b.Name = "Y"
b.Age = 30

结构体也仍然遵循可见性规则,要是定义结构体的字段时首字母为小写在其他包是不能直接访问该字段的。

可见性原则:
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。

因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。

对于匿名字段,可以使用如下方法对其进行操作:

p := new(Person)
p.ID = "123"
p.int = 10

通过这个例子也可以发现,对于一个结构体来说,每一种数据类型只能有一个匿名字段。

标签

在go语言中结构体除了字段的名称和类型外还有一个可选的标签tag,标记的tag只有reflect包可以访问到,一般用于orm或者json的数据传递,下面这段代码演示了如何为结构体打标签。

type Student struct {
    
    
	Name string `json:"name"`
	Age  int    `json:"age"`
}

可以使用go自带的json包将声明的结构体变量转变为json字符串。

func ToJson(s *Student) (string, error) {
    
    
	bytes, err := json.Marshal(s)
	if err != nil {
    
    
		return "", nil
	}
	return string(bytes), nil
}

如果没有给结构体打标签输出的json字符串如下所示:

{
    
    "Name":"james","Age":35}

如果给结构体打过标签之后输出的json字符串如下所示:

{
    
    "name":"james","age":35}

内嵌结构体

结构体作为一种数据类型也可以将其生命为匿名字段,此时称其为内嵌结构体,下面这段代码中将结构体A嵌入到结构体B中:

type A struct {
    
    
	X, Y int
}

type B struct {
    
    
	A
	Name string
}

通过内嵌结构体的方式可以在结构体B的变量下很方便的操作A中定义的字段。

b := new(B)
b.X = 10
b.Y = 20
b.Name = "james"

当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢?

  1. 外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式;
  2. 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。

例子:

type A struct {
    
    a int}
type B struct {
    
    a, b int}

type C struct {
    
    A; B}
var c C

//此时使用C.a访问是错误的
//编译报错:ambiguous selector

type D struct {
    
    B; b float32}
var d D
//使用d.b没问题,如果想要使用内层的b使用d.B.b


type A struct {
    
    a int}
type B struct {
    
    a, b int}

type E struct {
    
    A; B;a int}
var e E
//此时使用e.a访问会得到e中的a,也就是外层的a

方法

方法定义

方法与函数类似,只不过在方法定义时会在func和方法名之间增加一个参数,如下所示:

func (r Receiver)func_name(){
    
    
  // body
}

其中r被称为方法的接收者,例如下面这个例子:

type Person struct {
    
    
	name string
}

func (p Person) GetName() string {
    
    
	return p.name
}

其中GetName方法的接收者为p是Person结构体类型,也就是说我们为结构体Person绑定了一个GetName方法,可以使用如下的方式进行调用。

func main() {
    
    
	p := Person{
    
    
		name:"james",
	}
	fmt.Println(p.GetName())
}

方法接收者

对于一个方法来说接收者分为两种类型:值接收者和指针接收者。上面的GetName的接收者就是值接收者。再为Person结构体定义一个指针接收者:

func (p *Person)SetName(name string){
    
    
	p.name = name
}

使用值接收者定义的方法,在调用的时使用的其实是值接收者的一个拷贝,所以对该值的任何操作,都不会影响原来的类型变量。

但是如果使用指针接收者的话,在方法体内的修改就会影响原来的变量,因为指针传递的也是地址,但是是指针本身的地址,此时拷贝得到的指针还是指向原值的,所以对指针接收者操作的同时也会影响原来类型变量的值。

而且在go语言中还有一点比较特殊,使用值接收者定义的方法使用指针来调用也是可以的,反过来也是如此,如下所示:

func main() {
    
    
	p := &Person{
    
    
		name: "james",
	}
	fmt.Println(p.GetName()) //指针调用值接收者定义的方法

  p1 := Person{
    
    
		name: "james",
	}
	p1.SetName("kobe")
	fmt.Println(p1.GetName()) //反之
}

接口

接口定义

接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

type Namer interface {
    
    
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

Namer是一个接口类型

实现接口

  • 类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口

  • 实现某个接口的类型(除了实现接口方法外)可以有其他的方法

  • 一个类型可以实现多个接口

  • 接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)

package main

import "fmt"

type Shaper interface {
    
    
    Area() float32
}

type Square struct {
    
    
    side float32
}

func (sq *Square) Area() float32 {
    
    
    return sq.side * sq.side
}

func main() {
    
    
    sq1 := new(Square)
    sq1.side = 5

    var areaIntf Shaper
    areaIntf = sq1
    // shorter,without separate declaration:
    // areaIntf := Shaper(sq1)
    // or even:
    // areaIntf := sq1
    fmt.Printf("The square has area: %f\n", areaIntf.Area())
}

如果嵌入的结构体实现了某个接口那么对于外部的结构体有什么影响吗?

package main

import "fmt"

type Test interface {
    
    
	getX() int
}

type A struct {
    
    
	X int
}
type B struct{
    
    
	A
	X int
}
func (a A) getX() int{
    
    
	return a.X
}
func main() {
    
    
	a := A{
    
    
		X:5,
	}
	b := B{
    
    
		A:a,
		X:10,
	}
	fmt.Println(a.getX())// 5
	fmt.Println(b.getX())// 5
}

如果B也实现了接口,会输出10

类型断言

有些时候方法传递进来的参数可能是一个接口类型,但是我们要继续判断是哪个具体的类型才能进行下一步操作,这时就用到了类型断言,例子:

func IsSquare(a Shaper) bool {
    
    
	if v, ok := a.(*Square); ok {
    
    
		fmt.Println(v)
		return true
	}
	return false
}

上面的方法对传递进来的参数进行判断,判断其是否为Square类型,如果是Square类型的话就会将其进行转换为v,ok用来表示是否断言成功。

但是如果我们对于一个类型有好多种子类型要进行判断,这样写的话显然是有些复杂,可以使用如下这种方式:

func WhatType(a Shaper) {
    
    
	switch a.(type) {
    
    
	case *Square:
		fmt.Println("Square")
	case *Triangle:
		fmt.Println("Triangle")
	default:
		fmt.Println("error")
	}
}

空接口

空接口是一个比较特殊的类型,因为其内部没有定义任何方法所以空接口可以表示任何一个类型,比如可以进行下面的操作:

var any interface{
    
    }

any = 1
fmt.Println(any)

any = "hello"
fmt.Println(any)

any = false
fmt.Println(any)

参考

https://github.com/datawhalechina/go-talent

Go 入门指南

Go语言核心编程

猜你喜欢

转载自blog.csdn.net/i0o0iW/article/details/111490056