Go语言学习第三天
- 1Go语言学习第四天
- 2Go语言学习第三天本文
- 3Go语言学习第二天
- 4Go语言学习第一天

Go 入门笔记(三):结构体、自定义类型与接口
第三阶段的学习进入了 Go 的类型系统核心:结构体、结构体 tag、自定义类型、类型别名和接口。所有代码均在 Go 1.27.2 下实际运行验证,文中输出为真实结果。
一、结构体:Go 的”对象”
声明与嵌入
Go 没有 class,结构体(struct)承担了”对象”的角色:
type Student struct { Class // 匿名嵌入 name string}
type Class struct { name string className string}这里要纠正我注释里的说法:我当时把嵌入写成了”继承”,这不准确。Go 没有继承,Class 这种不带字段名的写法叫匿名嵌入(embedding),本质是组合。效果是 Class 的字段和方法会被”提升”到 Student 上,可以直接通过 s1.className 访问,看起来很像继承,但底层是 Student 持有了一个 Class。
嵌入带来的一个问题:同名字段的歧义。Student 和 Class 都有 name 字段,此时 s.name 指的是 Student 自己的 name,想拿 Class 的 name 必须写全路径:
func (s Student) getClassName() { // 有相同属性 name 时,需要加上结构体名才能获取到对应属性值 fmt.Println(s.Class.name) // 1-1 fmt.Println(s.className) // 1-1(className 不冲突,可以直接访问)}结构体方法:值接收者 vs 指针接收者
给结构体绑定方法的语法是在函数名前加接收者:
// 值接收者:只能读,修改不影响原对象func (s Student) getName() { fmt.Println(s.name)}
// 指针接收者:可以修改原对象func (s *Student) setName(name string) { s.name = name}这和上一阶段学的指针知识直接呼应:想修改结构体本身,必须用指针接收者。实际运行验证了这一点:
c1 := Class{name: "1-1", className: "1-1"}s1 := Student{Class: c1, name: "mirai"}s1.getName() // mirais1.setName("mirai1")s1.getName() // mirai1(修改生效)二、结构体 tag:控制 JSON 序列化
tag 是写在字段后面的反引号字符串,告诉标准库(或其他库)如何处理这个字段。配合 encoding/json 练习了三种:
type User struct { Name string `json:"name"` // 重命名:JSON 里显示为小写 name Age int `json:"age,omitempty"` // 零值时忽略该字段 Password string `json:"-"` // 始终忽略该字段}
u1 := User{Name: "mirai", Age: 0, Password: "123456"}jsonStr, _ := json.Marshal(u1)fmt.Println(string(jsonStr))实际输出:
{"name": "mirai"}三个 tag 的效果全部体现:
Name→ 输出为name;Age是 0(int 的零值),被omitempty忽略——注释里我特别记了这一点,0 也是零值,会被忽略,如果业务上 0 是合法值就要小心这个坑;Password被json:"-"完全排除,密码这类敏感字段不会泄露到 JSON 里。
三、自定义类型:给基础类型加”含义”
定义与方法
type Code int 创建了一个全新的类型:底层是 int,但和 int 是不同的类型,还可以给它绑定方法。练习是为前端返回状态码:
type Code int
const ( SuccessCode Code = 0 ServiceErrCode Code = 10001 NetErrCode Code = 10002)
func (code Code) response() (Code, string) { switch code { case SuccessCode: return code, "成功" case ServiceErrCode: return code, "服务错误" case NetErrCode: return code, "网络错误" } return code, "未知错误"}把方法挂在 Code 类型上,ServiceErrCode.response() 这种调用读起来很自然——“这个状态码对应的响应是什么”。这个文件本身没有 main 入口,我在临时环境补了调用验证:
request(1) // 10001 服务错误request(2) // 10002 网络错误request(3) // 10002 网络错误四、类型别名 vs 自定义类型
这两个长得极像,区别只在一个等号:
type aliaAction int // 自定义类型:新类型,与 int 不同type action = int // 类型别名:就是 int 的另一个名字,完全等价用 %T 打印类型,差别一目了然:
const aa1 aliaAction = 1const a1 action = 1
fmt.Printf("%T\n", aa1) // main.aliaAction(独立类型)fmt.Printf("%T\n", a1) // int(别名没有自己的类型)区别还体现在两处:
1. 能否绑定方法。 自定义类型可以绑定方法;别名不行——给 action(即 int)加方法会报错:
cannot define new methods on non-local type action因为别名指向的是非本地类型 int,不能给别的包/内置类型加方法。
2. 是否需要类型转换。 自定义类型和 int 之间比较、运算必须显式转换,两个方向都试了:
var num int = 1if int(aa1) == num { } // 自定义类型转 intif aa1 == aliaAction(num) { } // int 转自定义类型if a1 == num { } // 别名直接比较,无需转换一句话总结:自定义类型是”新品种”,别名只是”绰号”。 想基于已有类型扩展行为(加方法、做类型隔离)用自定义类型;只是想给类型起个好读的名字用别名。
五、interface:面向行为的抽象
定义与隐式实现
type SpeakerInterface interface { Speak()}
type Dog struct{ name string }type Cat struct{ name string }
func (d Dog) Speak() { fmt.Println(d.name + "汪汪") }func (c Cat) Speak() { fmt.Println(c.name + "喵喵") }Go 接口和其他语言最大的不同:实现是隐式的。 不需要写 implements,只要一个类型拥有接口要求的全部方法,它就自动实现了该接口。Dog 和 Cat 各自定义了 Speak(),它们就都是 SpeakerInterface。
于是函数可以面向接口编程,而不是面向具体类型:
// 只关心"会说话",不关心是狗是猫func speak(s SpeakerInterface) { s.Speak() // ...}
speak(dog) // qqz汪汪speak(cat) // coke喵喵空接口与 any
type PrintInterface interface{} // 空接口:不含任何方法
func Print(val any) { fmt.Println(val)}空接口 interface{} 没有任何方法要求,所以任何类型都实现了它,可以装任意值。Go 1.18 起官方给空接口加了个别名 any,两者完全等价:type any = interface{}。fmt 包里的 Println 接收的就是 ...any,这也是为什么它什么都能打印。
类型断言与类型判断
接口变量装的是具体类型,想”还原”回来用类型断言:
// 类型断言:s.(Dog),配合 comma-ok 判断comma, ok := s.(Dog)fmt.Println(comma, ok)实际输出验证了两种结果:
- 传入 dog 时:
{qqz} true—— 断言成功,拿到 Dog 值; - 传入 cat 时:
{} false—— 断言失败,comma是 Dog 的零值(空结构体打印为{})。
这里再次用到了 comma-ok(map 那里见过一次),不加 ok 的断言在失败时会直接 panic,所以实际代码里 comma-ok 形式更安全。
要分辨多种类型时,用 type switch 更优雅:
switch s.(type) {case Dog: fmt.Println(s) // {qqz}case Cat: fmt.Println(s) // {coke}default: fmt.Println("other")}s.(type) 只能用在 switch 里,这是它的专属语法。
一个意外收获:init 是包级的
运行这个文件时发现输出开头多了 1 2 3——是上一阶段 init 练习文件里的三个 init 函数打印的。这验证了:init 函数是包级别的,不管 main 在哪个文件,整个包的 init 都会在 main 之前全部执行。 算是顺手复习了上一课。
易错点与勘误汇总
| 位置 | 原注释/代码 | 纠正 |
|---|---|---|
| 11 结构体 | 注释写”继承” | Go 没有继承,匿名嵌入是组合,字段/方法被提升到外层结构体 |
| 12 tag | ——(注释正确) | 注意omitempty 会忽略 0、"" 等所有零值,0 是合法值时慎用 |
| 15 interface | ——(注释正确) | 类型断言不加 ok 失败会 panic;s.(type) 只能用于 switch |
总结
这一阶段把 Go 的类型体系串起来了:结构体替代 class,用组合(嵌入)代替继承;tag 让结构体字段能被 JSON 等外部系统正确处理;type X int 和 type X = int 一个等号之差,语义完全不同;接口用隐式实现做到了”面向行为编程”,配合类型断言和 type switch 处理多态。最大的感触是 Go 的设计非常克制——没有继承、没有 implements 关键字,却通过嵌入和隐式接口把面向对象的核心能力都覆盖到了。
后续值得探索:接口在标准库中的典型应用(如 io.Reader)、结构体方法接收者统一用指针的惯例、嵌入结构体的方法冲突处理。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!




