再谈Go的结构体

cmsmdn 2020-04-19

概述

结构体是由成员构成的复合类型。Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性。 结构体成员,也可称之为成员变量,字段,属性。属性要满足唯一性。 结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type) 数据层面,结构体是自定义数据类型,可以理解成是由一系列具有相同或不同类型的数据构成的数据集合。因此结构体也被称之为抽象数据类型(ADT,Abstract Data Type)。 在Go语言中,结构体承担着面向对象语言中类的作用。

Go语言中,结构体本身仅用来定义属性。还可以通过接收器函数来定义方法,使用内嵌结构体来定义继承。这样使用结构体相关操作Go语言就可以实现OOP面向对象编程了。

我们先看结构体相关语法,再看OOP相关的。

定义语法

type identifier struct {
    field1 type1
    field2 type2
    ...
}

// 或者 同一类型的字段,可以定义在一行
type identifier struct {
  field1, field2 int
}

实例化

Go语言提供了以下几种方法实例化:

// T表示结构体标识符,v结构体变量
var v T 
v := T{} // var v = T{}
v := new(T)
v := &T{} // var v = &T{}

以上方法中,var v T 和 v := T{} // var v = T{} 会返回结构体变量,而 v := new(T) 和 v := &T{} // var v = &T{} 会返回结构体指针。

值类型

注意,结构体是值类型,不是引用类型。因此使用不同方式实例化的,在赋值时效果时不一样的,需要注意。

var v T 和 v := T{} // var v = T{} 值传递。 v := new(T) 和 v := &T{} // var v = &T{} 引用地址传递。

Go语言会对结构体类型指针做自解析。也就是说,即使获得的是结构体指针,也不需要使用 *v 的语法。

v := &T{}
// 直接使用v.语法即可。自动解析了 *v
v.field
// 相当于,也可以这么用
(*v).field

初始化属性

使用类似于键值对的语法初始化结构体属性,但此处的键指的是结构体内字段:

v := T{
  field1: value1,
  field2: value2,
    …
}

如果我们初始化全部的结构体字段,可以按照定义顺序仅仅使用数据部分即可完成初始化:

// 要满足全部字段,按照定义顺序
v := T{
  value1,
  value2,
  value3,
}

成员访问运算符点号

要访问结构体成员,需要使用点号 . 操作符,格式为:

v.field
// 获取
fmt.Println(v.field)
// 设置
v.field = new-value

匿名结构体

匿名结构体没有类型名称,只有字段和类型定义,无须通过type关键字定义就可以直接使用。匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成。如下所示:

v := struct {
  field1 type1
  field2 type2
}{
  field1: value1,
  field2: value2,
}

注意,匿名结构体,必须要同时初始化,不能仅仅定义匿名结构体。 当需要使用一个临时结构体类型时,可以使用匿名结构体。

GO语言的断言

形如A.(T)/A.(*T)
其中A只能为interface, T为类型, 可以是interface 或者其他类型. string, int, struct等.

  • 若T为变量类型. 则用于判断转换为对应的变量类型. 这种用法可以使得一个函数接受多类型的变量.
    func VarType(var interface {})(err error){
      switch t := var.(type){
          case string:
              //add your operations
          case int8:
              //add your operations
          case int16:
              //add your operations
          default:
              return errors.New("no this type")
      }
    }
    
    //空接口包含所有的类型,输入的参数均会被转换为空接口
    //变量类型会被保存在t中
  • 若T为interface, 则可以用用来判断A这个接口类型是否实现了特定接口
    package main
    
    import (
        "fmt"
        "strconv"
    )
    
    type I interface{
        Get() int
        Put(int)
    }
    
    type P interface{
        Print()
    }
    //定义结构体,实现接口I
    type S struct {
        i int
    }
    func (p *S) Get() int {
        return p.i
    }
    func (p *S) Put(v int ) {
        p.i = v
    }
    func (p *S) Print() {
        fmt.Println("interface p:" + strconv.Itoa(p.i))
    }
    
    //使用类型断言
    func GetInt( some interface {}) int {
        if sp, ok := some.(P); ok {       // 此处断言some这个接口后面隐藏的变量实现了接口P 从而调用了. P接口中的函数Print.
            sp.Print()
        }
    
        return some.(I).Get()
    }
    
    func main(){
        s := &S{i:5}
        // a := GetInt(s)
        fmt.Println(GetInt(s))
    } 

Go语言接口判断实例

代码如下:

package main

import (
	"fmt"
)

type Demo struct {
	name string
}

type Helloxxx interface {
	Say()
}

func (p *Demo) Say() {
	fmt.Println(p.name,"Hello")
}

func main() {
	// 首先判断Demo是否实现了Helloxxx的接口,如果实现会返回一个接口对象,否则抛错
	// 接口判断结构体是否实现方式一:
	var a Helloxxx = (*Demo)(nil)
	// 实例一个demo
	demo := Demo{name:"wang"}
	// 将实例取地址传给接口a,注意由于接口a中实现的Say()需要指针,所以这里传入的是&
	a = &demo
	a.Say()
	// 打印a实例
	fmt.Println(a)

	// 接口判断,如果适合则返回一个接口实例,注意由于a中的Say()函数使用的指针,所以这里必须使用*Demo
	// 接口判断方式二:
	if ok,err := a.(*Demo); err != false {
		ok.Say()
		fmt.Println(ok)
	}

	// 调用结构体属性
	fmt.Println((&demo).name)
	v := &Demo{name:"wu"}
	fmt.Println((*v).name)
	(*v).Say()
	
	// 接口判断方式三:
	var _ Helloxxx = new(Demo)
}

Go语言的空值和零值

在 Go 语言中,布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串"",而指针、切片、映射、通道、函数和接口的零值则是 nil。

nil 是Go语言中一个预定义好的标识符,有过其他编程语言开发经验的开发者也许会把 nil 看作其他语言中的 null(NULL),其实这并不是完全正确的,因为Go语言中的 nil 和其他语言中的 null 有很多不同点。

  • nil标识符不能比较
    package main
     
    import (
        "fmt"
    )
     
    func main() {
        fmt.Println(nil==nil)
    }
    
    //invalid operation: nil == nil (operator == not defined on nil)
    //这点和 python 等动态语言是不同的,在 python 中,两个 None 值永远相等。  
  • nil不是关键字或保留字
    var nil = errors.New("my god")  
  • nil没有默认类型
    func main() {
        fmt.Printf("%T", nil)
        print(nil)
    }
    
    //go run .\main.go
    # command-line-arguments
    .\main.go:9:10: use of untyped nil  
  • 不同类型的nil指针一致
    package main
     
    import (
        "fmt"
    )
     
    func main() {
        var arr []int
        var num *int
        fmt.Printf("%p\n", arr)
        fmt.Printf("%p", num)
    }
    
    //0x0
    //0x0
  • nil 是 map、slice、pointer、channel、func、interface 的零值,且占据的空间大小不一致

    package main
     
    import (
        "fmt"
    )
     
    func main() {
        var m map[int]string
        var ptr *int
        var c chan int
        var sl []int
        var f func()
        var i interface{}
        fmt.Printf("%#v\n", m)
        fmt.Printf("%#v\n", ptr)
        fmt.Printf("%#v\n", c)
        fmt.Printf("%#v\n", sl)
        fmt.Printf("%#v\n", f)
        fmt.Printf("%#v\n", i)
    }
    
    /*
    map[int]string(nil)
    (*int)(nil)
    (chan int)(nil)
    []int(nil)
    (func())(nil)*/

make和new的区别

new:申请了内存,但是不会将内存初始化,只会将内存置零,返回一个指针。

make:申请了内存,返回已初始化的结构体的零值。

回到正文,虽然申请了内存,但占的内存其实并不多,并且在初始化后的一次gc中便会回收。所以还好。
同时也不存在效率问题,编译型语言,你懂的。

同时验证一个new和取地址和make的区别的代码:

func main() {
	a1 := new([]int)
	a2:= &[]int{}
	a3:= make([]int,0)

	fmt.Println(a1,a2,a3,a1==a1)
}
//&[] &[] [] true

对于内存的占用,今天有如下看法:

var _ Tester = (*Test)(nil)

//这样写和new的区别在于:new是编译的时候检查,这样写是运行的时候检查

相关推荐