使用type定义结构体,可以把结构体看做类型使用。必须指定结构体的字段(属性)名称和类型。
type User struct {
id int
name,addr string
score float32
}
package main
import "fmt"
type User struct {
id int
name, addr string
score float32
}
func main() {
// 1 声明
var u1 User // 这种方式声明结构体变量很方便,所有字段都是零值
fmt.Println(u1) //{0 0} 因为name和addr是string 所以只占位
fmt.Printf("%+v\n", u1) // 加上字段打印{id:0 name: addr: score:0}
fmt.Printf("%#v\n", u1) // 加上更多信息打印 main.User{id:0, name:"", addr:"", score:0}
//2 字面量初始化 推荐用这个
var u2 = User{} // 字段为零值
fmt.Printf("%+v\n", u2) //{id:0 name: addr: score:0}
//3 字面量初始化,field: value为字段赋值
u3 := User{id: 102}
fmt.Printf("%+v\n", u3) //{id:102 name: addr: score:0}
u4 := User{id: 12, score: 99, name: "zfl", addr: "xian"} //如果按照名称对应的话,无所谓顺序
fmt.Printf("%+v\n", u4) // {id:12 name:zfl addr:xian score:99}
u5 := User{103, "zfl", "xi'an", 99.9}//如果名称没有对应的话,必须按照结构体定义的顺序来给所有的字段值
fmt.Printf("%+v\n", u5) //{id:103 name:zfl addr:xi'an score:99.9}
}
type User struct {
Id int
Name, Addr string
Score float32
} //所以结构体的标识符User首字母大写,结构体的成员也需要大写,才可以包外可见
package main
import "fmt"
type User struct {
id int
name, addr string
score float32
}
func main() {
u1 := User{id: 12, score: 99, name: "zfl", addr: "xian"}
fmt.Printf("%+v\n", u1) //{id:12 name:zfl addr:xian score:99}
fmt.Println(u1.id, u1.addr) //12 xian
fmt.Println(u1.name, u1.score) //zfl 99
//直接修改id,通过字段名
u1.id = 104
fmt.Println(u1.id) //104
}
package main
import "fmt"
type User struct {
id int
name, addr string
score float32
}
// u称为receiver
// 等价于 func (User) string
func (u User) getName() string {
return u.name
}
func main() {
u1 := User{18, "zfl", "xa", 99.9}
fmt.Println(u1.getName())
}
package main
import "fmt"
type Point struct {
x, y int
}
func main() {
var p1 = Point{100, 200} //字面量初始化,一个实例
fmt.Printf("%T %[1]v\n", p1) //main.Point {100 200}
var p2 = &Point{5, 6}
fmt.Printf("%T %[1]v\n", p2) //*main.Point &{5 6}
var p3 = new(Point)
fmt.Printf("%T %[1]v\n", p3) //*main.Point &{0 0}
// 通过实例修改属性
p1.x = 10
fmt.Printf("%T, %[1]v\n", p1) //main.Point, {10 200}
// 通过指针修改属性
p2.x = 200
p3.x = 300
fmt.Printf("%T, %[1]v\n", p2) //*main.Point, &{200 6}
fmt.Printf("%T, %[1]v\n", p3) //*main.Point, &{300 0}
// p3.x中. 是 -> 的语法糖,更方便使用。等价于(*p3).x
fmt.Print(*p3, (*p3).x) // {300 0} 300
}
package main
import "fmt"
type Point struct {
x, y int
}
func test(p Point) Point {
fmt.Printf("4 %+v %p\n", p, &p) //4 {x:10 y:20} 0xc00001a160
return p
}
func main() {
var p1 = Point{10, 20}
fmt.Printf("1 %+v %p\n", p1, &p1) //1 {x:10 y:20} 0xc00001a0b0
p2 := p1
fmt.Printf("2 %+v %p\n", p2, &p2) //2 {x:10 y:20} 0xc00001a100
p3 := &p1
fmt.Printf("3 %+v %p\n", p3, p3) //3 &{x:10 y:20} 0xc00001a0b0
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
p4 := test(p1)
fmt.Printf("5 %+v %p\n", p4, &p4) //5 {x:10 y:20} 0xc00001a150
}
// 1 {x:10 y:20} 0xc00001a0b0
// 2 {x:10 y:20} 0xc00001a100
// 3 &{x:10 y:20} 0xc00001a0b0
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 4 {x:10 y:20} 0xc00001a160
// 5 {x:10 y:20} 0xc00001a150
可以看出,结构体是非引用类型,使用的是值拷贝。传参或返回值如果使用结构体实例,将产生很多副本。如何避免过多副本,如何保证函数内外使用的是同一个结构体实例呢?使用指针。
package main
import "fmt"
type Point struct {
x, y int
}
func test(p *Point) *Point {
p.x += 100
fmt.Printf("4 %+v %p\n", p, p)
return p
}
func main() {
var p1 = Point{10, 20} // 实例
fmt.Printf("1 %+v %p\n", p1, &p1)
p2 := p1
fmt.Printf("2 %+v %p\n", p2, &p2)
p3 := &p1
fmt.Printf("3 %+v %p\n", p3, p3)
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
p4 := test(p3)
fmt.Printf("5 %+v %p\n", p1, &p1)
fmt.Printf("6 %+v %p\n", p4, p4)
p4.x += 200
fmt.Printf("7 %+v %p\n", p1, &p1)
fmt.Printf("8 %+v %p\n", p4, p4)
}
1 {x:10 y:20} 0xc00001a0b0
2 {x:10 y:20} 0xc00001a100
3 &{x:10 y:20} 0xc00001a0b0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 &{x:110 y:20} 0xc00001a0b0
5 {x:110 y:20} 0xc00001a0b0
6 &{x:110 y:20} 0xc00001a0b0
7 {x:310 y:20} 0xc00001a0b0
8 &{x:310 y:20} 0xc00001a0b0
//说明,使用了同一个内存区域中的结构体实例,减少了拷贝。
匿名结构体:标识符直接使用struct部分结构体本身来作为类型,而不是使用type定义的有名字的结构体的标识符。
可以使用 var 、 const 、 := 来定义匿名结构体。
type定义结构体的标识符,可以反复定义其结构体实例,但是匿名结构体是一次性的。
//定义一个结构体类型,指代其的标识符名称为Point
type Point struct {
x, y int
}
//使用var定义一个变量point,后面跟类型,这个类型没有名字,只有结构体的本身
var Point struct {
x, y int
}
package main
import "fmt"
var Point struct {
x, y int
} // 定义Point是后面匿名结构体类型的,用零值
func main() {
fmt.Printf("%#v\n", Point) //struct { x int; y int }{x:0, y:0}
}
package main
import "fmt"
var message = struct {
id int
data string
}{1, "ok"}
func main() {
fmt.Println(message) //{1 ok}
fmt.Printf("%#v\n", message) //struct { id int; data string }{id:1, data:"ok"}
}
匿名结构体,只是为了快速方便地得到一个结构体实例,而不是使用结构体创建N个实例。
有时候属性名可以省略
package main
import "fmt"
type Point struct {
x int
int // 字段,匿名成员变量
bool // 匿名,必须类型不一样才能区分
}
var p1 = Point{1, 2, false}
var p2 = Point{x: 20, int: 5, bool: false}
func main() {
fmt.Println(p1) //{1 2 false}
fmt.Println(p2, p2.x, p2.int, p2.bool) //{20 5 false} 20 5 false
}
{1 2 false}
{20 5 false} 20 5 false
Go语言并没有从语言层面为结构体提供什么构造器,但是有时候可以通过一个函数为结构体初始化提供属性值,从而方便得到一个结构体实例。习惯上,函数命名为 NewXxx 的形式。
package main
import "fmt"
type Animal struct {
name string
age int
}
func NewAnimal(name string, age int) Animal {
a := Animal{name, age}
fmt.Printf("%+v,%p\n", a, &a)
return a
}
func main() {
a := NewAnimal("zfl", 20)
fmt.Printf("%+v,%p\n", a, &a)
}
{name:zfl age:20},0xc000008090
{name:zfl age:20},0xc000008078
//NewAnimal的返回值使用了值拷贝,增加了内存开销,习惯上返回值会采用指针类型,避免实例的拷贝。
package main
import "fmt"
type Animal struct {
name string
age int
}
func NewAnimal(name string, age int) *Animal {
a := Animal{name, age}
fmt.Printf("%+v, %p\n", a, &a)
return &a
}
func main() {
a := NewAnimal("zfl", 20)
fmt.Printf("%+v,%p\n", a, a)
}
{name:zfl age:20}, 0xc000008078
&{name:zfl age:20},0xc000008078
package main
import "fmt"
type Animal struct {
name string
age int
}
type Cat struct {
Animal // 匿名成员,可以使用类型名作为访问的属性名
color string
}
func main() {
var cat = new(Cat) // Cat实例化,Animal同时被实例化
fmt.Printf("%#v\n", cat)
cat.color = "black" // 子结构体属性
cat.Animal.name = "Tom" // 完整属性访问
cat.age = 20 // 简化写法,只有匿名成员才有这种效果
fmt.Printf("%#v\n", cat)
}
&main.Cat{Animal:main.Animal{name:"", age:0}, color:""}
&main.Cat{Animal:main.Animal{name:"Tom", age:20}, color:"black"}
//使用结构体嵌套实现类似面向对象父类子类继承(派生)的效果
//子结构体使用匿名成员能简化调用父结构体成员
Go语言中,可以为任意类型包括结构体增加方法,形式是 func Receiver 方法名 签名 {函数体} ,这个receiver类似其他语言中的this或self。
receiver必须是一个类型T实例或者类型T的指针,T不能是指针或接口。
package main
import "fmt"
type Point struct {
x, y int
}
func (p Point) getX() int {
fmt.Println("instance")
return p.x
}
func (p Point) getY() int {
fmt.Println("pointer")
return p.y
}
func main() {
p := Point{4, 5}
fmt.Println(p)
fmt.Println(p.getX(), (&p).getX())
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
fmt.Println(p.getY(), (&p).getY())
}
{4 5}
instance
instance
4 4
~~~~~~~~~~~~~~~~~~~~~~~~~~~
pointer
pointer
5 5
接收器receiver可以是类型T也可以是指针*T
package main
import (
"fmt"
)
type Point struct {
x, y int
}
func (p Point) setX(v int) {
fmt.Printf("1 %+v,%p\n", p, &p)
p.x = v
fmt.Printf("2 %+v,%p\n", p, &p)
}
func (p *Point) setY(v int) {
fmt.Printf("3 %+v, %p\n", p, p)
p.y = v
fmt.Printf("4 %+v, %p\n", p, p)
}
func main() {
p := Point{4, 5}
fmt.Printf("5 %+v, %p\n", p, &p)
p.setX(11) // 实例调用是值拷贝
p.setY(22) // 看似实例调用,实则是指针,操作同一处内存
fmt.Printf("6 %+v, %p\n", p, &p)
}
5 {x:4 y:5}, 0xc0000a6070
1 {x:4 y:5},0xc0000a60c0
2 {x:11 y:5},0xc0000a60c0
3 &{x:4 y:5}, 0xc0000a6070
4 &{x:4 y:22}, 0xc0000a6070
6 {x:4 y:22}, 0xc0000a6070
非常明显,如果是非指针接收器方法调用有值拷贝,操作的是副本,而指针接收器方法调用操作的是同一个内存的同一个实例。
如果是操作大内存对象时,且操作同一个实例时,一定要采用指针接收器的方法。
注意,深浅拷贝说的是拷贝过程中是否发生递归拷贝,也就是说如果某个值是一个地址,是只复制这个地址 ,还是复制地址指向的内容。
值拷贝是深拷贝,地址拷贝是浅拷贝,这种说法是错误的。因为地址拷贝只是拷贝了地址,因此本质上来讲也是值拷贝。
Go语言中,引用类型实际上拷贝的是标头值,这也是值拷贝,并没有通过标头值中对底层数据结构的指针指向的内容进行复制,这就是浅拷贝。非引用类型的复制就是值拷贝,也就是再造一个副本,这也是浅拷贝。因为你不能说对一个整数值在内存中复制出一个副本,就是深的拷贝。像整数类型这样的基本类型就是一个单独的值,没法深入拷贝,根本没法去讲深入的事儿。
简单讲,大家可以用拷贝文件是否对软链接跟进来理解。直接复制软链接就是浅拷贝,钻进软链接里面复制其内容就是深拷贝。
复杂数据结构,往往会有嵌套,有时嵌套很深,如果都采用深拷贝,那代价很高,所以,浅拷贝才是语言普遍采用的方案。