您的位置: 首页 > 通知公告

C语言结构体相关知识:新类型、底层类型、类型别名及定义形式


不少人在学完Go基础语法之后,面对结构体之时仍旧一知半解糊里糊涂,特别是那些从Java转过来的程序员,老是想着运用类的那套思维去套,结果写出来的代码既不高效,又没有Go的风格。实际上,Go的结构体就是它的“类”,然而比Java的POJO更加轻量级、更加灵活,唯有掌握它你才能够写出正宗的Go代码。

先从定义新类型说起

 type T1 int
 type T2 T1

于Go之中,你能够依据已然存在的类型,缔造出全然崭新的类型,这绝非仅为简单赋予别名这般。举例来说,倘若你书写type MyInt int,那么此MyInt便拥有了其独特个身份,它与int已然并非同一种事物了。这种机制于实际项目里格外具备效用,就好像你正着手重构一个陈旧项目,意欲逐步借助新类型替代旧类型,可又不想一朝更改完毕致使系统崩溃溃败,此时便能够先对新类型予以定义,使得新旧代码共存一段时期。

再一个常见情形是针对第三方包开展二次封装,你引入了一个外部的库,然而它的某些方法返回的值不太契合你的业务语义,在这种时候,你能够依据它的返回类型界定自身的新类型,接着附上更具业务意义的方法,如此一来,既实现了复用他人的代码,又能让自己所写的代码更为清晰且易于理解。

 type T = string

底层类型决定类型本质

这里存在着一个被称作底层类型的关键概念,仔细想来其即为在剔除光了所有自行定义的包装之后,处于最底层的那一个原生类型。举例来说,要是你定义了基于int的T1,随后又定义了基于T1的T2,那么实际上T1以及T2的底层类型均为int。这个底层类型具备相当的重要性,原因在于它对两个类型在本质层面上是否相同起着决定性的作用。

 // 定义结构体
 type Employee struct {
     Id   string
     Name string
     Age  int
 }

Go语言对显式类型转换有着要求:只有底层类型一样相同的变量,才能够去进行显式类型转换。这就无疑地给你给予了类型安全上的保障,不会出现因随便包裹了一层而导致搞出类型上的混乱这样的情况。比如说你所在的公司内部存在很多个部门在使用同样一种类似于概念的订单号,在此当中有的部门选用 string,表示着采用的是字符串类型,而有的部门是因为基于 string展开定义从而得到OrderID这种类型,如果在使用的时候不小心使这两个类型进行乱七八糟地混用,那么编译器就会毫不客气地报错,以此来逼迫着你去进行显式转换,进而能够有效避免出现低级错误。

结构体定义的基本规则

 // 三种初始化的方式
 func TestCreateObj(t *testing.T) {
     e := Employee{"001", "xxxx", 32} // 省略字段名。不建议
     t.Log(e)
 
     e2 := Employee{Name: "World", Age: 66}
     t.Log(e2)
 
     e3 := new(Employee) // 返回指针
     e3.Age = 111
     t.Log(e3)
 
     // 三种方式创建的对象的类型
     t.Logf("e : %T", e)   // obj_test.Employee
     t.Logf("e2 : %T", e2) // obj_test.Employee
     t.Logf("e3 : %T", e3) // *obj_test.Employee
 }

当进行结构体定义之际,字段名的大小写状态直接关乎其所具备的可见性情况。那些首字母呈现大写样式的字段,仿若具备public属性般,可以任由其它包予以随意访问;而首字母为小写的字段,则属于包内部私有的范畴,在外部包那根本无法看见此类情况。此种设计相较于Java的public/private而言更为直观,只要瞧上一眼字段名,便能够知晓其访问权限状况啦。

 e2 := Employee{Name: "World", Age: 66}

存有一个细节,众多人极易在此犯错:于你运用字面量对结构体进行初始化之际,倘若各个字段分别占用一行,那么处在最后位置的字段后面的逗号是必定要书写上去的。这般看上去不太顺眼,然而Go语言的编译器便是如此予以规定的。除此之外,在借助new创建结构体指针之时,能够直接运用点号去访问字段,就像e3.Age那般,Go语言将会自动为你实施解引用操作,无需像C语言那般书写成->。

     e2 := Employee{
         Name: "World",
         Age:  66,
     }

空结构体的妙用

 e3 := new(Employee) // 返回指针
 e3.Age = 111

空结构体struct{}不会占据任何内存空间,你能够将它视作一种纯粹的信号,在实际编程过程中,我们常常运用它来构建通知型channel;假定你启动一个goroutine去从事任务处理,主goroutine想要告知它停止,那么就能够定义一个chan struct{},向其中发送一个空结构体,接收方接收到此空结构体便意味着有事件已然发生了。

所采用的这种做法具备着令内存占用达到最小程度的益处,其性能亦是最为优良的。在所涉及的一些高性能中间件的源码范围之内,你常常能够见到如此这般的运用方式。举例来说,于Kubernetes的控制器代码当中,便是借助空结构体去管理属于资源的状态变更通知这一事项,以此来规避掉那些并非必要的内存开销情况。

结构体嵌套与语法糖

     type Empty struct {}
     var e Empty
     t.Log(unsafe.Sizeof(e))

结构体具备包含另外一个结构体当作字段的特性,并且Go给予了两种语法糖使代码更为简洁,其一能够省略字段名直接书写类型,如此一来字段名便默认与类型名保持一致,这称作嵌入字段,其二在访问时能够越过中间的结构体名直接去访问被嵌入的字段,仿佛这些字段是当前结构体自身所拥有的那般。

这款设计极其适配去做面向对象范畴内的组合模式,举例来说,你存有一个基础结构体内含日志方法,别的业务结构体径直嵌入它,便自然而然地具备了日志功能,无需像Java那般编写一连串重复的代理方法。诸多Go的ORM框架亦是运用这种形式,使得数据模型自动拥有基准的操作方法。

	var c = make(chan Empty) // 声明一个元素类型为Empty的channel
	c <- Empty{}             // 向channel写入一个“事件”

初始化与字段标签

Go的结构体具备“零值可用”这一特性,你对变量进行声明var book Book,它并非像Java那般为null,而是所有字段皆为对应类型的零值,这一特性避免了大量的空指针判断,你能够直接对其加以利用,比如计数器从0起始,字符串从空开始。

	type Reader struct {
		ReaderName string
		Age int
	}
	type Book struct {
		BookName string
		Reader Reader
	}

初始化之际万不可运用字段顺序作赋值操作,那般代码脆弱得犹如玻璃一般,一旦字段顺序发生改变便全然破碎了。务必要采用field:value的形式,其与顺序毫无关联,增添或删减字段亦不会产生影响。字段标签更是Go中的一项强大利器,例如书写json:"name,omitempty",便能够对JSON序列化时的行为加以控制,空字段会直接被忽略,这在编写API时可使返回数据简洁明了。

最后向大家提一个问题,你于实际项目里运用Go结构体之际,可曾碰到过因对底层类型缺乏了解进而致使类型转换出现错误的状况?欢迎于评论区去分享你的踩坑经历,认为这篇文章对你有益处的话,记得点赞并转发好使更多人能够看到!

	type Book struct {
		BookName string
		Reader
	}