Golang中Struct使用浅谈

一、golang里面的struct

  • Go面向对象编程的核心(struct)

Go 语言是一种静态类型的编程语言,这意味着,编译器需要在编译时知晓程序里每个值的类型。在Go中,通过使用关键字 struct,可以让用户创建一个自定义结构类型,并且结构类型允许通过组合一系列固定且唯一的字段甚至其他结构体来完成声明,便于用户定义具备丰富属性和操作的类型。

Go语言的结构体(struct)和其他语言的类(class)有同等的地位,给Go语言的面向对象编程提供了便利,但Go语言放弃了包括继 承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。

二、struct的基本使用

  • struct作为Go语言类型声明的核心,需要和Type字段功能组成类型系统语法的声明工作,基本的格式如下:
// 声明一个空类型
type structName struct{} 
//带有成员变量的类型
type structName struct{ 
   propertiesName type;
} 
//组合类型【嵌套类型】
type structNewName struct{ 
   propertiesName1 type;
   propertiesName2 structName;//上面的结构类型作为新类型的属性
} 

// 声明多个类型【归类组合】
type (
    structA struct{ 
       propertiesNameA type;
    } 
    structB struct{ 
       propertiesNameB type;
    }
    ...
)
  • 使用示例

申明一个类型:Cat,拥有属性name,然后定义一个行为方法Roar,用来发出叫声。我们可以看到,使用Cat类型时,需要进行初始化,类似Java中的获取一个实例对象,方法的调用使用类名.方法(参数类型)。

package main

import "fmt"

type Cat struct {
	Name string
}

func (c *Cat) Roar() {
	fmt.Println("喵喵喵")
}

func main() {
	var cat = &Cat{Name: "cily"}
	cat.Roar()
}

三、嵌套struct【组合】使用

  • 结构形式
type (
    structA struct{ 
       propertiesNameA1 type;
       propertiesNameA2 structB; //结构类型作为新类型的属性
    } 
    structB struct{ 
       propertiesNameB1 type;
       propertiesNameB2 structC;
    } 
    structC struct{ 
       propertiesNameC type;
    } 
)
  • 使用场景

嵌套struct是Go语言中组合语法,通过组合的特性可以组合声明多个属性、多个类型来组成具有丰富特性的新类型,即新对象,可以参考Java中的组合语法进行理解,不过Go的这种组合更简洁,便于理解,也正是这种特性使得Go语言的面向对象得以丰富。

  • 示例
package main

import (
	"fmt"
)

type (
	Car struct {
		factory string
		price   int
		engine  Engine
	}
	Engine struct {
		engineName string
	}
)

var instance *Car

func NewCar() *Car {
	if instance == nil {
		en := Engine{
			engineName: "default",
		}
		instance = &Car{
			factory: "",
			price:   0,
			engine:  en,
		}
	}
	return instance
}

func (c *Car) produceCar(factory string, price int, enName string) {
	c.price = price
	c.factory = factory
	c.engine.engineName = enName
}

func (c *Car) getCar() {
	fmt.Println(c)
}

func main() {
	car := NewCar()
	car.produceCar("factory one", 10, "沃尔沃")
	car.getCar()
}

四、踩坑注意

  • 在map使用时,特别结合struct场景,很容易因为map的指针的特性和Go底层的一些原理造成神奇的"指针现象",比如下例代码:
package main

import (
	"fmt"
	"strconv"
)

type (
	Strc struct {
		id   int
		info *Tmp
	}
	Tmp struct {
		id   int
		name string
		dec  string
	}
)

func main() {
	var stc = make([]*Strc, 0)
	var pr []Strc
	for i := 0; i < 5; i++ {
		var data = Strc{
			id: i,
			info: &Tmp{
				id:   i + 1, 
				name: "test",
				dec:  strconv.Itoa(i+1) + "test",
			},
		}
		pr = append(pr, data)
	}

	for _, i2 := range pr {
		s := &i2 //核心在这里
		stc = append(stc, s)
	}
	for i, s := range stc {
		fmt.Println(i, "--", s.info.id)
	}
}
结果:
0 -- 5 预期结果是1 【i+1】
1 -- 5 预期结果是2
2 -- 5 预期结果是3
3 -- 5 预期结果是4
4 -- 5 预期结果是5
  • 修复代码,和map的使用有关,只是在struct中应用时候需要注意,详情可以参考《GO语言设计与实现》
    基本原因:

一般来说,使用range遍历map的索引和元素时,会额外创建新的一片内存空间存储切片中的元素,循环中每一次迭代都会被重新赋值而被覆盖,赋值时也会触发复制,因为循环中获取返回变量的地址都完全一样,所以会发生神奇的"指针现象",不应该使用&i2,要么使用&i2[index],或者使用一个临时变量存储s := i2,然后使用&s。

package app

import (
	"fmt"
	"strconv"
)

type (
	Strc struct {
		id   int
		info *Tmp
	}
	Tmp struct {
		id   int
		name string
		dec  string
	}
)

func DDD() {
	var stc = make([]*Strc, 0)
	var pr []Strc
	for i := 0; i < 5; i++ {
		var data = Strc{
			id: i,
			info: &Tmp{
				id:   i + 1,
				name: "test",
				dec:  strconv.Itoa(i+1) + "test",
			},
		}
		pr = append(pr, data)
	}

	for _, i2 := range pr {
		s := i2  //核心在这里
		stc = append(stc, &s)
	}
	for i, s := range stc {
		fmt.Println(i, "--", s.info.id)
	}
}
结果:
0 -- 1
1 -- 2
2 -- 3
3 -- 4
4 -- 5