GO 从 0 到 1 系列:4 结构体

结构体

  • 结构体创建、访问与修改
  • 结构体指针
  • 结构体嵌套
  • 深拷贝与浅拷贝

1 结构体创建、访问与修改

1.定义结构体

//定义user结构体
type user struct {
    id int
    score float32
    enrollment time.Time
    name, addr string       // 多个字段类型相同时可以简写到一行里
}

// 通过 type 关键字中间是结构体名称,后面是类型为 struct
// 结构体种包含多个字段

2. var 初始化一个实例

// 声明,会用相应类型的默认值初始化 struct 里的,struct 类型的默认值为 nil
var u user 

// 相应类型的默认值初始化 struct 里的每一个字段
u = user{} 

// 赋值初始化
u = user{id: 3, name: "zgy"} 

// 赋值初始化,可以不写字段,但需要跟结构体定义里的字段顺序一致
u = user{4, 100.0, time.Now(), "zgy", "beijing"} 

3.短申明初始化实例

// 短声明初始化
u := User{}

// 短声明初始化并赋值
u :=User{id: 1,score: 99,name: "zgy",addr: "chongqing" enrollment: time.Now() }

4.访问与修改结构体

// 给结构体的成员变量赋值
u.enrollment = time.Now()

// 访问结构体的成员变量
fmt.Printf("id=%d, enrollment=%v, name=%s\n", u.id, u.enrollment, u.name)

1.1 初始化演示

package main

import (
    "fmt"
    "time"
)

type User struct {
    id         int
    score      float32
    name, addr string
    enrollment time.Time
}

func main() {
    var u User
    fmt.Printf("默认值:id %d,score %g ,name [%s],addr [%s],enrollment %v\n", u.id, u.score, u.name, u.addr, u.enrollment)

    u = User{score: 100, name: "zgy"}
    u.score = 99
    u.enrollment = time.Now()
    fmt.Println("赋值后:", u)
}

输出

# 可以看到输出之后的默认值都是结构体里面的每个字段类型的默认值
[13:21:32 root@go day2]#go run main.go 
默认值:id 0,score 0 ,name [],addr [],enrollment 0001-01-01 00:00:00 +0000 UTC
# 由于赋值后给 enrollment 中传递的是 time.Time 结构体
赋值后: {0 99 zgy  {13859730957717605692 98125 0x539800}}

1.2 结构体方法

方法和函数其实使用类型上相似,只是需要通过实例之后才能使用该结构体的方法

// 可以把 user 理解为 hello 函数的参数,即 hello(u user, man string)
func (u user) hello(man string) {
    fmt.Println("hi " + man + ", my name is " + u.name)
}

// 函数里不需要访问 user 的成员,可以传匿名,甚至_也不传
func (_ user) think(man string) {
    fmt.Println("hi " + man + ", do you know my name?")
}

代码演示能够访问该方法范例:

package main

import (
    "fmt"
    "time"
)

type User struct {
    id         int
    Score      float64
    name, addr string
    enrollment time.Time
}

// 绑定方法 hello
func (u User) hello(man string) string {
    return "hello " + man + " I m" + u.name
}

func main() {
    // 给 u 赋值为 User 类型
    u := User{Score: 100, id: 1, name: "zgy", addr: "cq"}
    fmt.Println(u.hello("李小龙"))
}

输出

[14:13:08 root@go day2]#go run main.go 
hello 李小龙 I mzgy

1.3 为任意类型添加方法

在 Go 中当我们定义了一个自定义类型之后,该类型就可以拥有方法

// 自定义类型
type UserMap map[int]User 

// 可以给自定义类型添加任意方法
func (um UserMap) GetUser(id int) User {
    return um[id]
}

代码演示:

package main

import "fmt"

// 定义 User 结构体
type User struct {
    name, addr string
    age        int
}

// 自定义 map 类型,value 为 User 结构体
type UserMap map[int]User

// 给 UserMap 绑定 gteuser 方法
func (um UserMap) GteUser(id int) User {
    um[1] = User{name: "zgy", addr: "cq", age: 12}
    return um[id]
}

// 绑定 delete 方法
func (um UserMap) Del(id int) {
    // 传入需要删除的 key
    delete(um, id)
}

func main() {
    // 赋值 um
    um := make(UserMap)
    // 获取 key 为 1 的 value
    fmt.Println(um.GteUser(1))

    um.Del(1)
    fmt.Println("删除后:", um)
}

输出

[18:09:05 root@go day2]#go run main.go 
{zgy cq 12}
删除后: map[]

1.4 结构体可见性

  • go 语言关于可见的统一规则:大写字母开头跨 package 也可以访问;否则只能本 package 内部访问
  • 结构体名称以大写开头时,package 外部可见,在此前提下,结构体中以大写开头在成员变量方法在package 外部也可见

1.先创建一个 job 目录

[13:36:54 root@go day2]#mkdir job

# 创建 struct/struct.go 文件
[13:43:20 root@go day2]#touch job/struct.go

2.在 job/struct.go 代码演示

// 包名首字母大写,别的包可以访问
package job

import "fmt"

// 结构体首字母大写外部包可访问
type User struct {
    // 字段首字母大写外部可访问
    Id   int
    Name string
}

// 方法首字母大写可跨包访问
// 并且这里
func (u *User) Hello() {
    fmt.Println("hello")
}

3.通过 go mod 命令初始化

[13:43:50 root@go day2]#go mod init day2
go: creating new go.mod: module day2
go: to add module requirements and sums:
        go mod tidy
[13:47:11 root@go day2]#go mod tidy

4.在 main 函数中调用

package main

import (
    "day2/job"
    "fmt"
)

func main() {
    user := job.User{
        Id:   1,
        Name: "job",
    }

    user.Hello()
    fmt.Println(user)
}

输出,可以看到在 main 包能够实现对 User 结构体的访问

[13:50:48 root@go day2]#go run main.go 
hello
{1 job}

1.5 匿名结构体

// 匿名结构体通过 var 定义,而 var 定义的话这个时候 stu 就不是一个结构体名称,而是一个变量名称
var stu struct { // 声明 stu 是一个结构体,但这个结构体是匿名的
    Name string
    Addr string
}

stu.Name = "zcy"
stu.Addr = "bj"

// 匿名结构体通常用于只使用一次的情况

代码演示:

package main

import "fmt"

func main() {
    // 定义匿名结构体
    var abc struct {
        Name string
        Sex  byte
    }
    abc.Name = "zgy"

    // 直接访问
    fmt.Println(abc)
}

输出

[14:17:48 root@go day2]#go run main.go 
{zgy 0}

1.5.1 结构体中含有匿名字段

代码演示

package main

import "fmt"

// 直接使用数据类型作为字段名,所以匿名字段中不能出现重复的数据类型
type Student struct {
    Name   string
    string // 匿名字段
    int    // 匿名字段
}

func main() {
    s := Student{}
    s.Name = "zgy"

    // 在访问的时候直接通过类型当作变量名称并赋值
    s.string = "重庆"
    s.int = 26
    fmt.Printf("Student value=%v,s.name = %s,s.string = %s,s.int=%d\n",
        s, s.Name, s.string, s.int)
}

输出

[18:38:28 root@go day2]#go run main.go 
Student value={zgy 重庆 26},s.name = zgy,s.string = 重庆,s.int=26

2 结构体指针

2.1 创建结构体指针

下面是声明结构体指针的几种方式:

// 方式一:
// 声明了 u 变量类型是 User
var u User

// 方式二:
// 通过取址符 & 得到指针,并赋给了 user ,这时候 user 就是指针变量
user := &u 

// 方式三:
// 直接创建结构体指针
user = &User{ 
    Id: 3, Name: "zgy", addr: "beijing",
}

// 方式四:
// 通过 new() 函数实体化一个结构体,并返回其结构体指针
user = new(User) 

2.2 构造函数

构造函数、也叫做工厂函数

对于一个普通的函数而言,返回一个结构体和返回一个结构体指针有什么区别呢?

  • 如果说只是返回结构体的话会发生值拷贝,本身在函数内部已经初始化好了结构体的字段,当该函数在 return 的时候会将初始化好的字段在从新拷贝一份在返回,而且这些字段只会创建一次并且只会将赋值后字段的内存空间地址返回而已
  • 如果说是返回指针的话,会将该结构体在内存中的地址进行返回
// 构造函数。返回指针是为了避免值拷贝
// 一般在工厂函数中我们都是用来初始化该结构体
func NewUser(id int, name string) *User {
    return &User{
        Id: id,
        Name: name,
        addr: "China",
        Score: 59,
    }
}

代码演示:

package main

import "fmt"

type Student struct {
    Name string
    Addr string
    Age  int
}

// 构造函数
func NewStudent(name, addr string, age int) *Student {
    return &Student{
        Name: name,
        Addr: addr,
        Age:  age,
    }
}

func main() {
    // 调用钩子函数返回 Student
    s := NewStudent("zgy", "重庆", 12)
    fmt.Println(s)
}

输出

[18:40:59 root@go day2]#go run main.go 
&{zgy 重庆 12}

2.3 普通函数接收指针

传递值拷贝:

// user 传的是值,即传的是整个结构体的拷贝。在函数里修改结构体不会影响原来的结构体
// 因为直接传入值的话在函数内部会发生一次拷贝,会将原先 user 结构体中的字段都拷贝一份,所以在函数中取到的 u.name 只是在外面的函数中传递进来的拷贝而已
func hello(u user, man string) {
    u.name = "杰克"
    fmt.Println("hi " + man + ", my name is " + u.name)
}

代码演示:

package main

import "fmt"

type Student struct {
    Name string
    Addr string
    Age  int
}

// 值传递
func hello(s Student, man string) {
    s.Name = "杰克"
    fmt.Println("hi" + s.Name + " my name is " + man)
}

func main() {
    s := Student{
        Name: "张三",
    }

    hello(s, "李四")
    fmt.Println("值拷贝:", s.Name)
}

输出:

[18:59:15 root@go day2]#go run main.go 
hi 杰克 my name is 李四       # 在 hello 函数中通过值拷贝 s.Name 变为了 杰克
值拷贝: 张三                 #  在 main 函数中 s.Name 依旧为张三并未实现修改

传递指针类型:

// 传的是 user 指针,在函数里修改 user 的字段会影响原来的结构体
// 因为传入指针的话是将该结构体的地址传入,在函数内部是通过地址去修改原始的 user 结构体内存空间
func hello2(u *user, man string) {
    u.name = "杰克"
    fmt.Println("hi " + man + ", my name is " + u.name)
}

代码演示:

package main

import "fmt"

type Student struct {
    Name string
    Addr string
    Age  int
}

// 传递 student 指针类型
func hello(s *Student, man string) {
    s.Name = "杰克"
    fmt.Println("hi " + s.Name + " my name is " + man)
}

func main() {
    s := Student{
        Name: "张三",
    }

    hello(&s, "李四")
    fmt.Println("指针传递:", s.Name)
}

输出

[18:59:19 root@go day2]#go run main.go 
hi 杰克 my name is 李四
值拷贝: 杰克

2.4 方法接收指针

其实本质上方法和函数是差不多的

方法接收指针和接收值拷贝的区别在于:

  • 接收指针 够实现对原有字段的赋值进行修改,因为对同一块内存地址进行操作
  • 接收只拷贝 不能 对原有字段的赋值进行修改,开辟一块新的内存进行一个值拷贝
  • 通常情况下我们使用方法都是想修改结构体的字段属性
// 把user理解为 hello() 的参数,即 hello(u user, man string)
func (u user) hello(man string) {
    u.name = "杰克"
    fmt.Println("hi " + man + ", my name is " + u.name)
}

// 可以理解为 hello2(u *user, man string)
func (u *user) hello2(man string) {
    u.name = "杰克"
    fmt.Println("hi " + man + ", my name is " + u.name)
}

方法值拷贝代码演示:

package main

import "fmt"

type Student struct {
    Name string
    Addr string
    Age  int
}

// 值传递
func (s Student) hello() {
    s.Name = "杰克"
    fmt.Println("hello 方法中:", s.Name)
}

func main() {
    s := Student{
        Name: "张三",
    }
    s.hello()
    fmt.Println("值拷贝:", s.Name)

}

输出

# 通过值拷贝输出可以看到在 main 函数中的 s.Nmae=张三 并未能修改
[19:11:12 root@go day2]#go run main.go 
hello 方法中: 杰克
值拷贝: 张三

方法指针传递代码演示:

package main

import "fmt"

type Student struct {
    Name string
    Addr string
    Age  int
}

// 指针传递
func (s *Student) hello() {
    s.Name = "杰克"
    fmt.Println("hello 方法中:", s.Name)
}

func main() {
    s := Student{
        Name: "张三",
    }
    s.hello()
    fmt.Println("指针传递:", s.Name)
}

输出

# 通过指针传递可以看到在 main 函数中的 s.Name 依旧修改为了 hello 方法中的杰克
[19:11:37 root@go day2]#go run main.go 
hello 方法中: 杰克
指针传递: 杰克

3 结构体嵌套

3.1 组合嵌套

组合嵌套也叫做组合,在嵌套结构体的时候需要对结构体指定字段名

组合嵌套如下伪代码:

// 定义了 user 结构体
type user struct {
    name string
    sex byte
}

// 在 paper 结构体中嵌套 user 结构体并赋值给 auther 字段
// 通过字段并且嵌套 user 结构体这叫做组合
type paper struct {
    name string
    auther user //结构体嵌套
}

/* 
    访问:
    先通过 new 一个 paper 
    想要访问嵌套结构体的字段,通过 p.auther.name 就能够访问到
*/
p := new(paper)
p.name = "论文标题"
p.auther.name = "作者姓名"
p.auther.sex = 0

代码演示:

package main

import "fmt"

type user struct {
    name string
    sex  byte
}

type paper struct {
    name   string
    auther user // 结构体组合嵌套
}

func main() {
    p := paper{
        name: "论文",
        // 在赋值的时候需要指定嵌套的结构体
        auther: user{
            name: "zgy",
            sex:  0,
        },
    }
    fmt.Println(p)

    // 访问 paper 结构体中的 name
    fmt.Println(p.name)

    // 嵌套结构体和父结构体字段冲突访问,就需要指定组合嵌套的结构体字段名称
    fmt.Println(p.auther.name)
}

输出

[19:44:03 root@go day2]#go run main.go 
{论文 {zgy 0}}
论文
zgy

3.2 匿名嵌套

匿名嵌套伪代码:

// 定义了 user 结构体
type user struct {
    name string
    sex byte
}

// 在嵌套的时候没有指定字段名直接嵌套结构体,这种叫做匿名嵌套
type vedio struct {
    length int
    name string
    user//匿名字段,可用数据类型当字段名
}

/* 
    访问:
    先通过 new 一个 vedio 
    想要访问匿名嵌套的字段,只需要写匿名嵌套的结构体名
*/
v := new(vedio)
v.length = 10
v.name = "视频名称"
v.user.name = "作者"
v.user.sex = 0 

代码演示:

package main

import "fmt"

type user struct {
    name string
    sex  byte
}

type vedio struct {
    length int
    name   string
    user   // 匿名嵌套 user 结构体
}

func main() {
    v := vedio{
        length: 10,
        name:   "电影名称",
        user: user{
            name: "作者",
            sex:  0,
        },
    }

    fmt.Println(v)

    // 问 vedio 结构体中的 name
    fmt.Println(v.name)

    // 嵌套结构体和父结构体字段冲突访问,就需要指定匿名嵌套的结构体字段名称
    fmt.Println(v.user.name)
}

输出

[19:45:58 root@go day2]#go run main.go 
{10 电影名称 {作者 0}}
电影名称
作者

3.3 嵌套字段名冲突

v := new(vedio)
v.length = 13
v.name = "视频名称"
v.user.sex = 0          // 通过字段名逐级访问
v.sex = 0               // 对于匿名字段也可以跳过中间字段名,直接访问内部的字段名
v.user.name = "作者姓名" // 由于内部、外部结构体都有 name 这个字段,名字冲突了,所以需要指定中间字段名

4 深拷贝和浅拷贝

  • 深拷贝,拷贝的是值,比如Vedio.Length
  • 浅拷贝,拷贝的是指针,比如Vedio.Author
  • 深拷贝开辟了新的内存空间,修改操作不影响原先的内存
  • 浅拷贝指向的还是原来的内存空间,修改操作直接作用在原内存空间上

4.1 浅拷贝


如上图定义 user 结构体只有 name 字段,并且还定义了 vedio 结构体包含了视频长度和视频作者,接着我创建了 vedio1 并且给 length 赋值 25 分钟,作者就是吴承恩,然后将 vedio1 赋值给了 vedio2 由于是通过 = 号赋值所以发生值拷贝,虽然 vedio1 和 vedio2 都是一样得内容,但是他们两个变量却是不同的内存空间。

在 vedio2 的内存空间修改 name 是不会影响到 vedio1 的 name 字段值

代码演示:

package main

import "fmt"

type User struct {
    Name string
}

type Vedio struct {
    Length int
    Author User
}

func main() {
    v1 := Vedio{}
    v1.Length = 25
    v1.Author.Name = "吴承恩"

    v2 := v1
    v2.Author.Name = "罗贯中"
    fmt.Println("浅拷贝 v1.Author.Name  :", v1.Author.Name)
    fmt.Println("浅拷贝 v2.Author.Name  :", v2.Author.Name)
}

输出

[20:15:15 root@go day2]#go run main.go 
浅拷贝 v1.Author.Name  : 吴承恩
浅拷贝 v2.Author.Name  : 罗贯中

# 通过输出我们发现 Author.Name 字段的属性并没有发发生变换

4.2 深拷贝


如上图给 Author 对象赋值的是 User 指针类型的结构体,如果我们给 Author.Name 字段赋值为吴承恩,然后将 vedio1 赋值给了 vedio2 由于是通过 = 号赋值所以发生值拷贝。

并且在 vedio1 赋值给 vedio2 的时候 vedio2 的 name 字段指向的其实是 vedio1.Author.Name 的内存地址

在 vedio2 的内存空间修改 name 会影响到 vedio1 的 name 字段值,由于 vedio2.Author.Name 指向的是 author.Name 的值所以修改的时候其实是对该指针指向的内存的值进行了修改

代码演示:

package main

import "fmt"

type User struct {
    Name string
}

type Vedio struct {
    Length int
    Author *User
}

func main() {
    v1 := Vedio{
        Length: 25,
        Author: &User{
            Name: "吴承恩",
        },
    }

    v2 := v1
    v2.Author.Name = "罗贯中"
    fmt.Println("深拷贝 v1.Author.Name  :", v1.Author.Name)
    fmt.Println("深拷贝 v2.Author.Name  :", v2.Author.Name)
}

输出

[20:19:03 root@go day2]#go run main.go 
深拷贝 v1.Author.Name  : 罗贯中
深拷贝 v2.Author.Name  : 罗贯中

# 在深拷贝中发现修改了 v2 的 name 字段也会间接的修改掉 v1 的 name

5 结构体 slice 传参


如上图 User 本身是一个结构体,users 是由 User 结构体构成的一个 sliece ,这时候 users 中就能够存放很多 User 的切片元素

接着我写了一个func update_users 的函数,在这个函数中我修改了 users[0] 中的 name 字段为光绪

传递的是 slice 是对 slice 结构体的三个对象 array unsafe.Pointer,len int,cap int 进行值拷贝,由于 slice 的 array 字段是指针,所以当我们修改了 users[0].Name = "光绪" 就会间接修改了原有的 "康熙",因为他们都是在对同一块内存空间进行操作

代码演示:

package main

import "fmt"

type User struct {
    Name string
}

// 接收 User 结构体切片
func update_users(users []User) {
    // 将下标为 0 的 User 结构体 Name 字段修改为了 光绪
    users[0].Name = "光绪"
}

func main() {

    // 赋值 users 为 []User ,Name 字段为 康熙
    users := []User{
        {Name: "康熙"},
    }

    fmt.Println("传参前", users[0].Name)

    update_users(users)

    fmt.Println("传参后", users[0].Name)
}

输出

# 通过传递参数已经修改
[20:32:06 root@go day2]#go run main.go 
传参前 康熙
传参后 光绪
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇