一、go与面向对象


go是一门面向对象的语言吗? 是,也不是

严格来说在go中没有面向对象(oop)的说法,它不像其它面向对象编程语言一样,有类、继承、对象、构造函数、析构函数这些概念,但是可以使用结构体(struct)来实现面向对象的特性和功能,它类似于其它编程语言中的类,不过和传统的面向对象有很大的区别.

我们知道面向对象三大特性:封装、继承、多态,都可以通过结构体来实现

  • 封装: 把数据存储到对象的内部,隐藏内部细节,对外可以可见或不可见,按标识符规则大写开头表示外部可见,小写只在内部可见

  • 继承: 把公共的字段和方法提取出来复用,通过在结构体嵌入匿名字段(结构体)方式实现,这种方式也叫组合

  • 多态: 鸭子类型,通过接口实现,接口是方法的类型,只要实现接口中的方法即可

接口是go中的重要的特性,通过接口(interface)关联,耦合性低,非常灵活,所以也叫面向接口编程

在go中实现面向对象比较简洁、优雅,就拿继承来说是通过组合实现的,接口也非常灵活,从设计上就是为了避免传统面向对象那些比较复杂实现方式.

 

二、结构体


每种数据结构都有自己的类型,用于不同的场景, 像int、map、slice等都只能保存单一、相同的数据类型,在表达复杂数据时候无法满足要求,因此我们可以使用结构体来表达复杂的数据结构。

结构体是任意类型字段的集合,从而可以组成复杂的数据结构,定义方式如下

type 标识符 struct {
    字段1 类型
    字段2 类型
    字段3 类型
    字段n 类型
}

要点:

  • 通过type、结构体名、struct关键字定义一个结构体类型,也可以为结构体添加方法

  • 结构体内部可以有若干字段以及对应的类型

  • 结构体是值类型

  • 如果多个字段的类型一致,可以合并参数的类型,写在一行

  • 结构体名与字段名同样遵守大小写可见性规则

示例:

type Cat struct {
    Name  string
    Age   int
    Color string
}

表示定义了一个Cat结构体,它拥有Name、Age、Color三个字段,类型分别是 string、int、string。 这个结构体就可以当成某种类型使用

要点

  • 字段也叫属性或成员变量,意思都差不多
  • 结构体是值类型
  • 和变量的规则一样,大写名字对外可见

三、结构体初始化与赋值


定义了结构体就可以初始化与赋值, 有下面几种方式

方式1: 通过字段赋值

// 声明一个Cat类型的结构体cat1
var cat1 Cat

//打印cat1,没有赋值所以为各个字段的零值 
fmt.Println(cat1)   // { 0 }

// 指定字段赋值
cat1.Name = "小白"
cat1.Age = 20
cat1.Color = "灰色"

// 打印cat1的详细信息(访问字段)
fmt.Printf("cat1的详细: Name=%v, Age=%v, Color=%v\n", cat1.Name, cat1.Age, cat1.Color)  // cat1的信息: Name=小白, Age=20, Color=灰色

注:

cat1可以看成是Cat结构体(类)的一实例,或者说叫结构体变量。
不能指定一个没有在结构体中定义字段赋值,比如cat1.xxx = ooo, 因为xxx字段没有在结构体中定义(和动态语言不一样)

方式2: 按照字面量赋值

// 按照顺序个字段赋值,特点是一一对应,而且不能少也不能多
var cat1 Cat = Cat{"小白", 10, "灰色"}
cat2 := Cat{"小黑", 20, "红色"}

fmt.Printf("cat1: Name:%v, Age:%v, Color:%v\n", cat1.Name, cat1.Age, cat1.Color) // cat1: Name:小白, Age:10, Color:灰色

fmt.Printf("cat2: Name:%v, Age:%v, Color:%v\n", cat2.Name, cat2.Age, cat2.Color) // cat2: Name:小黑, Age:20, Color:红色
cat3 := Cat{"小红", 45} // 报错

// 指定字段赋值,类似于key:value方式,特点是不用考虑顺序,而且可选赋值
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

cat1 := Person{
    Name: "zhang",
        Age:  10,
}

cat2 := Person{
    Age:  20,
    Name: "li",
}

// age不赋值
cat3 := Person{
    Name: "alice",
}
fmt.Println(cat1, cat2, cat3)    

方式3: 通过结构体指针

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // cat1是一个类型为*Person的指针
    var cat1 *Person = &Person{}
    // 打印指针的值
    fmt.Printf("%p\n", cat1) // 0xc00004a420
    // 指针解引用
    fmt.Printf("%v\n", *cat1) // { 0}
    // 通过指针给字段赋值
    (*cat1).Name = "zhang"
    cat1.Age = 20 // 这种方式也赋值,语法糖,与(cat1*).Age = 20等价 //Nmae:zhang, Age:20
    fmt.Printf("Nmae: %v, Age: %v\n", cat1.Name, (*cat1).Age)

    // cat2是一个类型为*Person的指针
    cat2 := &Person{"li", 10}
    fmt.Printf("Nmae: %v, Age: %v\n", cat2.Name, (*cat2).Age)

    // cat3是一类型为*Person的指针(通过new函数)
    cat3 := new(Person)
    fmt.Printf("%p\n", cat3)  //0xc00004a480
    fmt.Printf("%v\n", *cat3) //{ 0}
        (*cat3).Name = "bob"
    cat3.Age = 45
    fmt.Printf("Nmae: %v, Age: %v\n", cat3.Name, (*cat3).Age) // Nmae: bob, Age: 45
}

注: 如果是结构体指针变量,可以通过(*变量).字段引用或设置字段值,也可以通过变量.字段方式,前者是标准的写法,后者是语法糖

四、结构体使用细节


1. 如果结构体的字段包含了引用类型,需要初始化才能使用

type Person struct {
    Name   string
    Slice  []int          // int切片类型字段
    Family map[int]string // map类型字段
}

func main() {
    var p Person
    p.Name = "zhang"
    p.Slice[0] = 100  // 报错, 切片没有初始化不能使用
    p.Family[1] = 200 // 报错, map没有初始化不能使用
}

Person结构体包含了引用字段,不能直接使用, 应该先初始化在使用

func main() {
    var p Person
    p.Name = "zhang"
        // 初始化字段
    p.Slice = make([]int, 10)
    p.Family = make(map[int]string, 20)

    //字段赋值
    p.Slice[0] = 100
    p.Family[1] = "xxoo"
    fmt.Println(p.Slice, p.Family) // 输出: [100 0 0 0 0 0 0 0 0 0] map[1:xxoo]
}

2. 不同的结构体互不影响

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    var p Person
    p.Name = "zhang"
    p.Age = 10
    fmt.Printf("p.Name=%v, p.Age=%v\n", p.Name, p.Age) // p.Name=zhang, p.Age=10

    p2 := p
    p2.Name = "alice"
    p2.Age = 20
    fmt.Printf("p2.Name=%v, p2.Age=%v\n", p2.Name, p2.Age) // p.Name=zhang, p.Age=10
}

在这里例子中p2是结构体p的拷贝,p2的Name和Age属性和p是不一样的,因为结构体是值类型,赋值给另一个变量得到的是拷贝(新的内存空间)。

3. 只有相同个数的字段并且类型一致的结构才能转换

        package main

        import "fmt"

        type A struct {
            Name string
            Age  int
        }

        type B struct {
            Name string
            Age  int
        }

        func main() {
            var a A
            var b B
            a = b     // 错误,不能隐式的转换
            c := B(a) // 正确
            fmt.Println(c, b)
        }

4. 结构体是值类型,内存分配是连续的

package main

import (
    "fmt"
)

type Person struct {
    Id     int
    Score  int
    Gender int
}

func main() {
    // 实例化出对象p
    p := Person{1, 2, 3}
    fmt.Println(&p.Id, &p.Score, &p.Gender) // 0xc00005a140 0xc00005a148 0xc00005a150   # 连续地址,后面在前面加8
}

5. 给字段打tag,用于序列化或反序列

可以给结构体的字段打tag,可以用于序列化和反序列化,程序间的交互

    package main

    import (
        "encoding/json"
        "fmt"
    )

    type Person struct {
        Name string `json:"name"` // 给字段添加json格式的tag
        Age  int    `json:"age"`
        City string `json:"city"`
    }

    func main() {
        // 实例化出对象p
        p := Person{"Bob", 20, "Beijing"}
        // 通过json包的Marshal格式化,返回一个字节切片和错误类型
        jsonstr, _ := json.Marshal(p)
        // 转成字符串并打印
        fmt.Println(string(jsonstr)) // {"name":"Bob","age":20,"city":"Beijing"}
    }

五、匿名结构体


匿名结构体,也就是没有名字的结构体,类似于匿名函数, 可以当做一个字段集合来用

package main

import "fmt"

func main() {
    // 声明了一个匿名结构体并直接初始化
    x := struct {
        Name string
        age int
    }{"huangwiemin", 20}  
    // 打印x
    fmt.Println(x)
    // 查看类型
    fmt.Printf("%T\n", x)       // struct { Name string; age int }
    // 引用字段
    fmt.Println(x.Name, x.age)  // huangwiemin 20
}