Go 学习笔记(15):结构体

栏目: Go · 发布时间: 5年前

内容简介:结构体(定义结构体的一般语法如下:

结构体( Struct )是复合类型,可以封装属性和操作(即字段和方法)。

Go 中的结构体类似面向对象编程中的轻量级类,但 Go 中没有类的概念,所以结构体尤为重要。

创建

定义结构体的一般语法如下:

type identifier struct {
    field1 type1
    field2 type2
    ...
}
复制代码
  • 如果不需要 field ,可以将其命名为 _
  • 结构体成员遵循 Go 的导出原则,一个结构体可以同时包含导出和未导出的成员

初始化

定义结构体类型: Person

type Person struct {
    Name   string
    Gender string
    Age    uint8
}
复制代码

结构体的 零值 是每个成员都是零值:

// 零值化
var p1 Person
fmt.Printf("%+v\n", p1)  // {Name: Gender: Age:0}

// 选择器
p1.Name = "Ming"
p1.Age = 32
fmt.Printf("%+v\n", p1)  // {Name:Ming Gender: Age:32}
复制代码

使用字段名:

  • 使用字段名时,顺序可以打乱;
  • 忽略字段名时,需要严格按照定义的顺序,且数量得对应;
  • 未提供初始值的字段将使用该字段的类型的零值
p2 := Person{Name: "Qiang", Gender: "Male", Age: 30}
fmt.Printf("%+v\n", p2)    // {Name:Qiang Gender:Male Age:30}

p3 := Person{Gender: "Male", Name: "Hua"}
fmt.Printf("%+v\n", p3)    // {Name:Hua Gender:Male Age:0}

p4 := Person{"Jack", "Male", 30}
fmt.Printf("%+v\n", p4)    // {Name:Jack Gender:Male Age:30}
复制代码

使用字段赋值时和 map 的键值对的写法有点类似,但请注意区别。

使用 new() :使用 new 函数给一个新的结构体变量分配内存,它返回指向* 已分配内存的指针

var p5 *Person
fmt.Println(p5 == nil)  // true

p5 = new(Person)
fmt.Printf("%+v\n", p5)   // &{Name: Gender: Age:0}

var p6 *Person = new(Person)
fmt.Printf("%+v\n", p6)  // &{Name: Gender: Age:0}

p7 := new(Person)
fmt.Printf("%+v\n", p7)  // &{Name: Gender: Age:0}

// 指针也有选择器,也可以使用类似属性的操作
p7.Name = "Ming"
fmt.Printf("%+v\n", p7)  // &{Name:Ming Gender: Age:0}
复制代码

混合字面量语法( composite literal syntax )是一种简写,底层仍然会调用 new ()

p8 := &Person{"Dai", "Male", 42}
fmt.Printf("%+v\n", p8)  // &{Name:Dai Gender:Male Age:42}
复制代码

注意, new(Type)&Type{} 是等价的 ,见内存布局。

上述中的 p1p2 等通常被称做类型 Person 的一个实例( instance )或对象( object )。

简写

当字段的类型相同时,可以写在同一行:

type T struct {
    a, b int
}

t := T{1, 2}
fmt.Println(t.a)  // 1
fmt.Println(t.b)  // 2
复制代码

工厂方法

Go 中没有类的概念,不存在 OOP 中的构造方法,但是我们可以使用工厂方法实现类似的行为。

按照惯例,工厂函数的名字以 newNew 开头:

package main

import (
    "fmt"
    "unsafe"
)

type Person struct {
    name string
    age uint8
}

func NewPerson(name string, age uint8) *Person {
    // 注意,这个返回的是局部变量的地址.
    // 如果在 C++ 中是个典型的错误,Go 中没问题
    return &Person{name, age}
}

func main() {
    p := NewPerson("Jack", 20)
    fmt.Printf("%+v\n", p)  // &{name:Jack age:20}
    fmt.Println(unsafe.Sizeof(Person{}))  // 查看一个实例占用了多少内存
}
复制代码

这类似于面向对象中的实例化一个类。

选择器

无论一个 结构体类型 还是一个 结构体类型指针 ,都使用同样的选择器符来引用结构体的字段:

type Person struct {
    name string
    age  uint8
}
p1 := Person{"Jack", 20}      // 结构体类型变量
p2 := &Person{"Duncan", 42}   // 指向一个结构体类型变量的指针

fmt.Println(p1.name, p1.age)  // Jack 20
fmt.Println(p2.name, p2.age)  // Duncan 42
复制代码

内存布局

用图说明结构体类型 Point 的实例和指向它的指针的内存布局:

type Point struct { 
    x, y int 
}
复制代码
Go 学习笔记(15):结构体

new(Point)&Point{} 返回的是指针。

Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中。

举例:

package main

import (
    "fmt"
    "strings"
)

type Person struct {
    name string
    age uint8
}

// 这是方法,详细请见后续笔记
func update(p *Person) {
    p.name = strings.ToUpper(p.name)
}

func main() {
    p1 := Person{"Duncan", 43}
    update(&p1)
    fmt.Println(p1.name)  // DUNCAN

    p2 := new(Person)
    p2.name = "Curry"     // 指针也能使用选择器,Go自动做了转换,不像C++中那样需要使用 -> 操作符
    update(p2)
    fmt.Println(p2.name)  // CURRY

    (*p2).name = "Tony"   // 也可以取指针的值
    update(p2)
    fmt.Println(p2.name)  // TONY

    p3 := &Person{"Durant", 30}
    update(p3)
    fmt.Println(p3.name)  // DURANT
}
复制代码

匿名字段

匿名字段,即没有名字的字段,声明时只指定类型。

  • 类型必须是命名的类型或指向一个命名的类型的指针
  • 匿名字段的名字默认是其类型的名字,因此不能同时包含两个相同类型的匿名字段,这会导致名字冲突
  • 匿名字段也有可见性的规则约束
type Person struct {
	name string
	age int
	int  // 匿名字段,不能有两个匿名字段 int
	string
}

p1 := Person{}
fmt.Printf("%+v\n", p1)  // {name: age:0 int:0 string:}
fmt.Println(p1.int)  // 匿名字段的名字是其类型名

p2 := new(Person)
p2.int = 10
p2.string = "abc"
fmt.Printf("%+v\n", p2)  // &{name: age:0 int:10 string:abc}

p := Person{"a", 10, 10}
fmt.Printf("%+v\n", p)  // {Name:a Age:10 int:10}
复制代码

内嵌结构体

当一个结构体嵌套进另一个结构体在 Go 语言中也是很常见的,举例:

type Address struct {
	province string
	city string
}

type User struct {
	name string
	age int
	address Address
}

u := &User{
	name: "Ming",
	age: 30,
	address: Address{
		province: "Jiangsu",
		city: "Nanjing",
	},
}
fmt.Printf("%+v\n", u)  // &{name:Ming age:30 address:{province:Jiangsu city:Nanjing}}
复制代码

当结构体作为匿名成员的时候,会有一些特殊的用法:

type Address struct {
	province string
	city string
}

type User struct {
	name string
	age int
	Address
}

// 方法一:正常直观方式定义
u1 := &User{
	name: "Ming",
	age: 30,
	Address: Address{
		province: "Jiangsu",
		city: "Nanjing",
	},
}
fmt.Printf("%+v\n", u1)  // &{name:Ming age:30 Address:{province:Jiangsu city:Nanjing}}

// 同上
var u2 User
u2.name = "Qiang"
u2.age = 35
u2.Address = Address{province: "Jiangsu", city: "Suzhou"}
fmt.Printf("%+v\n", u2)  // {name:Qiang age:35 Address:{province:Jiangsu city:Suzhou}}

// 方法二:匿名嵌入时可以直接访问叶子属性而不需要给出完整的路径
var u3 User
u3.name = "A"
u3.age = 40
u3.province = "Jiangsu"
u3.city = "Wuxi"
fmt.Printf("%+v\n", u3)  // {name:A age:40 Address:{province:Jiangsu city:Wuxi}}

// 但下面的方式是错误的,编译不能通过
// cannot use promoted field Address.province in struct literal of type User
// cannot use promoted field Address.city in struct literal of type User
u4 := User{
	name: "A",
	age: 29,
	province: "Jiangsu",
	city: "Wuxi",
}
fmt.Printf("%+v\n", u4)
复制代码

内嵌结构体可以用来实现类似 OOP 中的继承

命名冲突

当两个字段拥有相同的名字(可能是继承来的名字)时:

  • 外层名字会覆盖内层名字(但是两者的内存空间都保留),利用此特性可以实现字段或方法重载
  • 如果相同的名字在同一级别出现了两次,当使用这个名字时将会引发一个错误(不使用没关系)。
type Group struct {
	name string
	id int
}

type Team struct {
	id int
}

type User struct {
	name string
	Group
	Team
}

var u User

// 当结构体中的字段与匿名成员内的字段冲突时,结构体的字段覆盖匿名成员的字段,但匿名成员的字段仍然存在
u.name = "A"
fmt.Printf("%+v\n", u)  // {name:A Group:{name: id:0} Team:{id:0}}

u.Group.name = "B"
fmt.Printf("%+v\n", u)  // {name:A Group:{name:B id:0} Team:{id:0}}

// 当匿名成员中的字段出现冲突,使用时会引起错误
fmt.Println(u.Group.id)  // 正常
fmt.Println(u.Team.id)   // 正常
fmt.Println(u.id)        // ambiguous selector u.id
复制代码

其它

标签

tag 是结构体的元信息,是一个附属于字段的字符串,可以是文档或其他的重要标记。

标签的内容不能在一般的编程中使用,只有包 reflect 能获取它。

根据Go conventions, tag 有一些约定:

  • tag 字符串是可选的空格分隔的 key-value
  • key 是非空字符串,由空格(' ')、引号(' ')和冒号(':')以外的非控制字符组成
  • value 使用双引号 "Go 字符串
  • 检查字段是否为可导出
import (
	"encoding/json"
	"fmt"
	"reflect"
)

type User struct {
	Name string  `json:"xxx"`
	Sex string   `json:"sex"`
	Age uint8
}

func main() {
	u := User{"Ming", "male", 30}

    // 通过 reflect 包获取 tag
	fmt.Println(reflect.TypeOf(u).Field(0).Tag)  // json:"xxx"
	fmt.Println(reflect.TypeOf(u).Field(1).Tag)  // json:"sex"
	fmt.Println(reflect.TypeOf(u).Field(2).Tag)

	data, _ := json.Marshal(u)
	fmt.Printf("%s\n", string(data))  
	// {"xxx":"Ming","sex":"male","Age":30}
	// 注意字段名,Age 没添加 tag,默认为原来的
	
	// User 结构体的非导出字段不会被 json 访问到
}
复制代码

递归结构体

结构体类型可以引用自身的指针类型,这就是递归结构体。

定义一个二叉树:

type tree struct {
    value int
    left *tree
    right *tree
}
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

文明之光(第四册)

文明之光(第四册)

吴军 / 人民邮电出版社 / 2017-3 / 69.00元

计算机科学家吴军博士继创作《浪潮之巅》、《数学之美》之后,将视角拉回到人类文明史,以他独具的观点从对人类文明产生了重大影响却在过去被忽略的历史故事里,选择了有意思的几十个片段特写,有机地展现了一幅人类文明发展的画卷。《文明之光》系列创作历经整整四年,本书为其第四卷。 作者所选的创作素材来自于十几年来在世界各地的所见所闻,对其内容都有着深刻的体会和认识。《文明之光》系列第四册每个章节依然相对独......一起来看看 《文明之光(第四册)》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器