实际编程时,经常需要用相关的不同类型的数据来描述一个数据对象。C#中有类(Class),结构(Struct),当然类就不介绍了。Golang中叫结构体(C,C++好像还是结构体),单词还是Struct,无论是在Golang还是C#,struct都是一个值类型。

struct 结构体

1.C#的结构struct

1.1 构造函数

  • struct有默认无参构造函数,不能再显式定义这个无参构造函数,编译器始终会生成一个默认的构造器

结构不能包含显式的无参数构造函数,默认构造器会把所有字段的自动初始化

public struct Position
        {
    		//public Position()
            //{} // 这是不允许的
    
            public double Lon { get; set; }
            public double Lat { get; set; }
        }
//没有自定义构造函数,可不适用new
Position positon;
positon.Lon = 39.26;
positon.Lat = 115.25;
  • 自定义的有参构造函数必须初始化所有的字段
public struct Position
        {
            //自定义构造函数需要初始化所有字段、属性
            public Position(double lon, double lat)
            {
                Lon = lon;
                Lat = lat;
            }

            //结构中不能实例属性或字段初始值设定项
            //public double Lon { get; set; }=5.5;
            public double Lon { get; set; }
            public double Lat { get; set; }
        }
//有参构造函数,必须使用new为struct类型的变量赋值
Position positon = new Position(39.26, 39.26);

1.2 方法

结构是可以包含自己的方法。

public struct Position
{
    //自定义构造函数需要初始化所有字段、属性
    public Position(double lon, double lat)
    {
        Lon = lon;
    }

    //结构中不能实例属性或字段初始值设定项
    //public double Lon { get; set; }=5.5;
    public double Lon { get; set; }
    public double Lat { get; set; }
    
    //重写方法
    public override string ToString() => $"经度:{Lon}, 纬度{Lat})";
}

虽然struct在实际开发过程中使用频率较低,但是使用时需要注意:

  • 结构类型变量作为参数传递给方法从方法返回结构类型值时,将复制结构类型的整个实例。这可能会影响高性能方案中涉及大型结构类型的代码的性能。 通过按引用传递结构类型变量,可以避免值复制操作。 使用 ref、out 或 in 方法参数修饰符,指示必须按引用传递参数。使用 ref 返回值按引用返回方法结果。在Golang中也会存在这个问题,下一节会提到。

2.Golang的结构体struct

2.1 定义

像定义函数类型那样 type开头,只是把func()换为关键字struct

type person struct {
	name string
	age  int8
}

//结构体匿名字段
type student struct {
	string
	int
}

func main() {
    var p1 person
	p1.name = "RandyField"
	p1.age = 28
    
    //匿名结构体
    var user struct {
		Name string
		Age  int
	}
	user.Name = "Randy"
	user.Age = 18  
}

2.2* 结构体指针–重点

2.2.1 new

只要是指针,都可以用new()来进行分配内存地址以达到初始化的目的:

type person struct {
	name string
	age  int8
}
//结构体匿名字段
/*
    这里匿名字段的说法并不代表没有字段名,
    而是默认会采用类型名作为字段名,
    结构体要求字段名称必须唯一,
    因此一个结构体中同种类型的匿名字段只能有一个。
*/
type Student struct {
	string
	int
}
func main() {
    //new分配结构体实例的指针(内存地址)   实例化
	var p2 = new(person)
    fmt.Printf("the type of p2 is %T\n", p2) //*main.person
    
    //没有初始化的结构体 所有的成员变量都是对应类型的零值
	fmt.Printf("p2=%#v\n", p2)
    
   	stu := &Student{}
	stu.int = 18
	stu.string = "中学生"
}
the type of p2 is *main.person
p2=&main.person{name:"", age:0}

2.2.2 &

这个看起来比new()方便

type person struct {
	name string
	age  int8
}
func main() {
	p3 := &person{} //使用&对结构体进行取地址操作=> 使用new实例化
	p3.name = "kobe"
	p3.age = 30    //这是语法糖
	(*p3).age = 29 //其实底层是这样的
}
  • 初始化

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。这点跟C#存在有参构造函数的结构是不一致。

func main() {
    p4 := person{
            name: "RandyField",
            age:  18,
        }
    
    fmt.Printf("p4=%#v\n", p4)

	//结构体指针初始化
	p5 := &person{
		name: "RandyField",
		age:  28,
	}
    fmt.Printf("p5=%#v\n", p5)
}
  • 另类初始化

不建议使用,但是为了能看懂别人的开源代码,还是知道机制为妙。

p6 := &person{
    "RandyField",
    28,
}
fmt.Printf("p6=%#v\n", p6)

2.3* 空结构体

特殊地:空结构体是不占用空间的。

var v struct{}
fmt.Println(unsafe.Sizeof(v))  // 0

2.4 构造函数

Golang是没有构造函数的,但是我们可以通过方法去创建一个,返回struct类型。

type person struct {
	name string
	age  int8
}

// 复杂的结构体,值拷贝性能开销会比较大,故返回结构体指针。
func newPerson(name string, age int8) *person {
	return &person{
		name: name,
		age:  age,
	}
}

2.5 方法

Golang结构体的方法并不像C#的结构那样直接就在结构的{}中定义即可。它必须分开定义,这就出现一个难题,定义的这个方法是属于这个结构体的,并不希望其他地方都能使用,现在分开定义,怎么办?

接收者应运而生,指明这个方法是属于结构体,只能通过结构体来调用。

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}
type person struct {
	name string
	age  int8
}

func (p *person) MakeMoney(work string) (res string) {
	return "赚钱了"
}
  • 接收者既可以是指针类型,也可以是值类型
    • 值类型,如果做出了操作,只针对副本有效

使用指针类型场景:

  • 需要修改接收者中的值
  • 接收者是拷贝代价比较大的大对象
  • 如果有某个方法使用了指针类型接收者,其他的方法也应该使用指针类型接收者。

2.5.1 类型定义 与 类型别名

方法的接收者不仅仅可以是结构体,还可以是类型定义:

type NewInt int  //类型定义 新类型  可以作为方法的接收者
type MyInt = int //类型别名 编译完成时并不会有`MyInt`类型, 这个不能作为方法接收者的
func (m NewInt) Say() {
	fmt.Println("我其实是int。")
}

2.6 嵌套结构体

type student struct {
	name string
	age  int
}

type middleSchoolStudent struct {
	lesson []string
	*student
}

func (stu *student) play(sport []string) {
	for _, v := range sport {
		fmt.Println(stu.name, "参加如下运行项目:", v)
	}
}
func (m *middleSchoolStudent) learn() {
	for _, v := range m.lesson {
		fmt.Println(m.name, "学习如下课程:", v)
	}
}

func main(){
    s := &middleSchoolStudent{
        lesson: []string{"语文", "数学", "英语", "物理", "化学"},
        student: &student{
            name: "小明",
            age:  13,
        },
    }

    s.learn()
    s.play([]string{"篮球", "足球", "乒乓球"})
}
小明 学习如下课程: 语文
小明 学习如下课程: 数学
小明 学习如下课程: 英语
小明 学习如下课程: 物理
小明 学习如下课程: 化学
小明 参加如下运行项目: 篮球
小明 参加如下运行项目: 足球
小明 参加如下运行项目: 乒乓球

有点像继承,其实这又是一个语法糖, s.play([]string{"篮球", "足球", "乒乓球"}),内部是s.student.play([]string{"篮球", "足球", "乒乓球"})

如果在定义时,给嵌套结构体一个字段名称:

type middleSchoolStudent struct {
	lesson []string
	stu *student
}

调用方式必须为:s.stu.play([]string{"篮球", "足球", "乒乓球"})

2.7 Tag

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

`key1:"value1" key2:"value2"`
type MiddleSchoolStudent struct {
	Lesson []string
	Name   string `json:"studentName"`
	Age    int    `json:"studentAge"`
}
func main(){
	ms := &MiddleSchoolStudent{
		Name:   "小明",
		Age:    13,
		Lesson: []string{"语文", "数学", "英语", "物理", "化学"},
	}
	data, err := json.Marshal(ms)
	if err != nil {
		fmt.Println("json marshal failed")
		return
	}
	fmt.Printf("json:%s\n", data)
	// fmt.Println(data)
}
json:{"Lesson":["语文","数学","英语","物理","化学"],"studentName":"小明","studentAge":13}

注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

再次强调:这个系列并不是教程,如果想系统的学习,博主可推荐学习资源。