
文章目录1. 结构体基础1.1 什么是结构体1.2 结构体初始化1.2.1 零值初始化1.2.2 字面量初始化推荐 ⭐1.3 结构体访问1.4 结构体的内存布局1.5 结构体标签 ⭐1.6 匿名结构体1.7 结构体比较1.8 匿名嵌入2. 结构体和方法2.1 方法的定义2.2 值接收和指针接收2.3 Go 自动转换机制2.4 嵌套结构体的方法调用3. 小结本系列文章Java 转 go 学习 - 项目管理Java 转 go 学习 - 基本语法Java 转 go 学习 - 类型转换Java 转 go 学习 - 流程控制结构Java 转 go 学习 - 数组和切片Java 转 go 学习 - mapJava 转 go 学习 - 函数1Java 转 go 学习 - 函数2前两篇文章中我们学习了函数的用法这篇文章就来学习结构体。1. 结构体基础1.1 什么是结构体首先来看下结构体的定义type结构体名struct{字段名1类型1字段名2类型2// ...}Go 中不存在类结构体就对应了 Java 中的类属于纯数据容器没有继承和方法函数传参是值传参结构体里面只能定义属性比如下面我们可以定义一个用户信息结构体。typeUserstruct{IDintUsernamestringEmailstringAgeintIsActivebool}关于结构体也有一些命名规范结构体名大写字母开头导出/公开如 User。字段名大写字母开头表示公开其他包可访问小写字母开头表示私有仅当前包可访问。1.2 结构体初始化Go 提供了多种初始化结构体的方式。1.2.1 零值初始化零值初始化就是声明之后不赋值这种情况下结构体里面的字段自动初始化为类型的零值int0, string, boolfalse, 切片/指针nil。typeUserstruct{IDintUsernamestringEmailstringAgeintIsActivebool}funcmain(){varu1 User fmt.Println(u1)fmt.Println(u1.Age,u1.IsActive,u1.Username,u1.Email,u1.Age)}输出如下{00false}0falsetruetrue0可以看到默认初始化的结构体不为 nil但是初始化出来的结构体里面的各个字段都是默认零值。1.2.2 字面量初始化推荐 ⭐推荐用这种方式来初始化也就是使用{}直接赋值这样可读性比较高字面量初始化可以用下面几种方式。首先是指定字段名在{}中指定想要初始化的字段。// 指定字段名初始化functest2(){u2:User{ID:1,Username:alice,Email:aliceexample.com,Age:25,IsActive:true,}fmt.Println(u2)// {1 alice aliceexample.com 25 true}}这种方式是比较推荐的不容易出错也可以按顺序赋值不需要指定字段名这种就不推荐了。functest2(){// 不指定字段名// {1 bob bobexample.com 30 true}u3:User{1,bob,bobexample.com,30,true}fmt.Println(u3)}如果是不指定字段名的情况下需要将全部字段都初始化指定字段名可以一部分初始化没有指定的字段就默认设置成零值。functest2(){// 指定部分字段名u4:User{ID:1,Username:alice2,Email:alice2example.com,}fmt.Println(u4)// {1 alice2 alice2example.com 0 false}}最后也可以用new关键字去创建结构体new比较特殊的是会返回结构体的指针所有字段都是零值。functest3(){u5:new(User)u5.ID3u5.Usernamejohnfmt.Println(u5)// {3 john 0 false}}用 new 的方式创建结构体默认等价于User{}。1.3 结构体访问不管是结构体还是结构体指针都可以通过.访问或者修改结构体里面的属性如果是结构体指针通过.访问属性的时候Go 会自动解引用访问具体的值当然也可以(*struct).xxx这样来访问。functest4(){t1:User{ID:1,Username:A}t2:User{ID:2,Username:B}fmt.Println(t1.ID)// 1fmt.Println(t2.ID)// 2}1.4 结构体的内存布局Go 语言中结构体和结构体所包含的数据在内存是以连续块的方式存在的哪怕是结构体里面嵌套了其他的结构体不像 Java 的引用类型一个对象和里面包含的对象可能在不同的内存空间下面我们就用嵌套结构体来看下这些特性。// 基础结构体Point2个int32字段共8字节typePointstruct{Xint32// 4字节Yint32// 4字节}// 嵌套结构体Rect包含2个Point 1个uint32typeRectstruct{Min Point// 嵌套Point占8字节Max Point// 嵌套Point占8字节Coloruint32// 4字节}functest5(){// 初始化 Rect 实例r:Rect{Min:Point{X:1,Y:2},Max:Point{X:3,Y:4},Color:0xFF0000,// 红色}// 打印每个字段的内存地址和大小fmt.Printf(Rect 实例起始地址: %p\n,r)fmt.Printf(Min.X 地址: %p大小: %d 字节\n,r.Min.X,unsafe.Sizeof(r.Min.X))fmt.Printf(Min.Y 地址: %p大小: %d 字节\n,r.Min.Y,unsafe.Sizeof(r.Min.Y))fmt.Printf(Max.X 地址: %p大小: %d 字节\n,r.Max.X,unsafe.Sizeof(r.Max.X))fmt.Printf(Max.Y 地址: %p大小: %d 字节\n,r.Max.Y,unsafe.Sizeof(r.Max.Y))fmt.Printf(Color 地址: %p大小: %d 字节\n,r.Color,unsafe.Sizeof(r.Color))fmt.Printf(Rect 总大小: %d 字节\n,unsafe.Sizeof(r))}上面结果输出如下Rect 实例起始地址:0xc00001c0d8Min.X 地址:0xc00001c0d8大小:4字节 Min.Y 地址:0xc00001c0dc大小:4字节 Max.X 地址:0xc00001c0e0大小:4字节 Max.Y 地址:0xc00001c0e4大小:4字节 Color 地址:0xc00001c0e8大小:4字节 Rect 总大小:20字节可以看到的是Rect中的所有字段都是依次连续分布在内存的Min.X0xc00001c0d8→Min.Y4 字节→Max.X4 字节→Max.Y4 字节→Color4 字节也就是说结构体里面嵌套结构体Point的数据也是直接嵌入Rect的内存块里面不会说 Point 单独存放到一个地方然后Rect里面存引用地址这种是值类型的分布可以看到的是这样分布好处就是没有内存碎片读取性能比较高。但是同样的写法如果换成 Java 就不行了因为 Java 中类是引用类型比如换成下面的写法。public class MemoryDemo{static class Point{intx;inty;Point(intx,inty){this.xx;this.yy;}}static class Rect{Point min;// 引用类型Point max;// 引用类型intcolor;// 基本类型Rect(Point min,Point max,intcolor){this.minmin;this.maxmax;this.colorcolor;}}public static voidmain(String[]args){Point p1newPoint(1,2);Point p2newPoint(3,4);Rect rnewRect(p1,p2,0xFF0000);// Rect堆对象}}这种情况下由于 Point 是引用类型自然 Rect 中存储的就是堆内存的地址执行堆中的 Point 对象了两种写法内存分布大概如下面图中所示。但是对于结构体如果想要也像 Java 这样存储地址那结构体里面存结构体的指针就行了也就是换成下面的定义方式。typeRectPointerstruct{Min*Point// 结构体指针, 8 字节Max*Point// 结构体指针, 8 字节Coloruint32// 4字节}这时候再输出Rect 实例起始地址:0xc0000080a8Min.X 地址:0xc00000a088大小:4字节 Min.Y 地址:0xc00000a08c大小:4字节 Max.X 地址:0xc00000a0a0大小:4字节 Max.Y 地址:0xc00000a0a4大小:4字节 Color 地址:0xc0000080b8大小:4字节// 最后一个就不是连续的了Rect 总大小:24字节这时候会发现Max、Min和Color的内存地址就不是连续的了。1.5 结构体标签 ⭐结构体字段后面可以跟一个反引号包裹的字符串称为 Tag给这个字段打上标签。作用在结构体后面打上标签之后通过reflect可以获取到标签的内容读取元数据。格式key:value。最常见的就是 JSON 序列化了我们给结构体中的字段打上标签和 JSON 中的字段一一对应。typeProductstruct{IDintjson:id// JSON key: idNamestringjson:name// JSON key: namePricefloat64json:price// JSON Key: priceIsSoldbooljson:is_sold,omitempty// omitempty 表示如果是 false, 序列化时忽略这个字段Secretstringjson:-// 序列化时完全忽略该字段}然后测试下序列化和反序列化。functest7(){p:Product{ID:101,Name:test,Price:999.99,IsSold:false,Secret:internal_code,}// 序列化data,_:json.Marshal(p)fmt.Println(string(data))p2:Product{}// 反序列化_json.Unmarshal(data,p2)fmt.Println(*p2)}最终输出如下{id:101,name:test,price:999.99}{101test999.99false}1.6 匿名结构体这种不需要定义type直接使用struct{ 字段名 } { 初始值 }创建一次性结构体。functest8(){// 1. 定义并初始化user:struct{NamestringAgeint}{Name:Tom,Age:18,}// 2. 创建结构体切片, 在切片中使用users:[]struct{IDintRolestring}{// 初始化写法 1{ID:1,Role:admin},// 初始化写法 2{2,guest},}fmt.Println(users)fmt.Println(user)// [{1 admin} {2 guest}]// {Tom 18}}又或者作为函数返回值返回。// 3. 作为函数返回值funcgetPoint()struct{X,Yint}{returnstruct{X,Yint}{X:10,Y:20}}1.7 结构体比较结构体比较主要看里面的字段如果结构体的所有字段都可比较如 intstringbool数组等那么结构体本身可以用比较然后包含切片、Map、函数这些引用类的属性就不能直接用比较因为地址肯定是不相等的需要用reflect.DeepEqual。typePstruct{X,Yint}funcmain(){p1:P{1,2}p2:P{1,2}fmt.Println(p1p2)// true}如果加上切片类型就会编译的时候报错。typePstruct{X,YintDate[]int}funcmain(){p1:P{1,2,make([]int,0)}p2:P{1,2,make([]int,0)}fmt.Println(p1p2)// Invalid operation: p1 p2 (the operator is not defined on P)}1.8 匿名嵌入上面1.6小结我们学习了结构体的嵌套也就是结构体里面定义结构体实际上结构体的名字也可以省略掉这样就相当于把内层结构体的字段嵌套进外层结构体中 这种就是匿名嵌入。typeAddressstruct{CitystringNamestring}typePersonstruct{NamestringAddress// 匿名嵌入}funcmain(){p:Person{Name:John,Address:Address{City:Shanghai,Name:John-inner,},}// 可以直接访问内层结构体的字段fmt.Println(p.City)fmt.Println(p.Name)fmt.Println(p.Address.Name)}但是要注意像上面的例子中如果内层结构体有一个字段和外层的一样通过p.Name访问到的是外层的字段内层需要通过p.Address.Name去访问才行。2. 结构体和方法2.1 方法的定义在 Go 中方法就是带有接收者的函数之前我们说过结构体就类似 Java 的类但是结构体里面不能定义方法所以 Go 中我们可以通过这种方式将函数绑定到某个结构体上从而实现像 Java 中的类方法的写法下面是基本语法。func(r ReceiverType)MethodName(params)ReturnTypes{// 方法体// 可以通过 r 访问结构体的字段}可以理解成就是在原来函数的基础上加了(r ReceiverType)就可以绑定到某个结构体上。// 1. 定义结构体typeRectanglestruct{Widthfloat64Heightfloat64}// 2. 计算面积func(r Rectangle)Area()float64{returnr.Width*r.Height}// 3. 计算周长func(r Rectangle)Perimeter()float64{return2*(r.Widthr.Height)}funcmain(){rect:Rectangle{Width:10,Height:5}// 调用方法使用点号fmt.Printf(面积: %.2f\n,rect.Area())// 面积: 50.00fmt.Printf(周长: %.2f\n,rect.Perimeter())// 周长: 30.00}2.2 值接收和指针接收特性值接收者(r T)指针接收者(r *T)定义func (r T) Method()func (r *T) Method()数据拷贝拷贝结构体的副本方法内修改不影响原结构体。传递地址方法内修改直接影响原结构体。性能小结构体没问题大结构体拷贝开销大。无论结构体多大只拷贝指针8字节效率高。适用场景1. 不需要修改原数据2. 类型是不可变的如 map, slice 包装器3. 小型简单结构体1. 需要修改原结构体字段2. 结构体很大避免拷贝3. 一致性原则推荐默认使用调用方式val.Method()或ptr.Method()(Go 自动解引用)ptr.Method()或val.Method()(Go 自动取地址)推荐使用*T的写法下面举个例子。typeCounterstruct{Countint}func(c Counter)Inc1(){c.Countfmt.Println(值类型计数当前值: ,c.Count)}func(c*Counter)Inc2(){c.Countfmt.Println(引用类型计数当前值: ,c.Count)}functest2_2(){c:Counter{Count:0}// 1. 调用值接收者方法c.Inc1()fmt.Println(调用后 (值):,c.Count)// 2. 调用指针接收者方法c.Inc2()fmt.Println(调用后 (指针):,c.Count)// 值类型计数当前值: 1// 调用后 (值): 0// 引用类型计数当前值: 1// 调用后 (指针): 1}可以看到值接收者方法是不会修改原始结构体的而指针接收者会修改。2.3 Go 自动转换机制正常来说我们都是用的指针接收者这样就能避免有时候调用方法修改了结构体有时候没有修改结构体。如果方法定义成了值接收者通过(c).Inc1()调用也是没问题的编译器会自动转成(*(c)).Inc1()也就是c.Inc1()。c.Inc1()(c).Inc1()// 和上面是一样的fmt.Println(调用后 (值):,c.Count)如果方法定义成了指针接收者通过上面例子中的c.Inc2()来调用Go编译器会自动转换成(t).Inc2()。2.4 嵌套结构体的方法调用如果结构体 A 匿名嵌入了结构体 B那么 B 的方法也可以通过 A 直接调用来看下面例子。packagemainimportfmttypeLoggerstruct{Prefixstring}// Logger 方法func(l*Logger)Log(msgstring){fmt.Printf([%s] %s\n,l.Prefix,msg)}typeServicestruct{Logger// 嵌入Namestring}functest(){s:Service{Logger:Logger{Prefix:Service},Name:Payment,}// 直接调用结构体 Logger 的 Log 方法s.Log(Starting service...)}funcmain(){test()// [Service] Starting service...}可以看到 Service 嵌入结构体 Logger 后可以直接调用 Log 方法打印日志。3. 小结结构体的文章就先学习到这下一篇文章来看下接口的内容。如有错误欢迎指出