视频加载失败

Go语言学习第三天

2057 字
10 分钟
Go语言学习第三天
Go语言学习第三天

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() // mirai
s1.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 = 1
const 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 = 1
if int(aa1) == num { } // 自定义类型转 int
if 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)、结构体方法接收者统一用指针的惯例、嵌入结构体的方法冲突处理。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Go语言学习第三天
https://firefly.cuteleaf.cn/posts/go语言学习第三天/
作者
MIRAI
发布于
2026-10-17
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
MIRAI
心之所向,素履以往
公告
欢迎来到我的博客!
分类
标签
最新动态
站点统计
文章
5
分类
1
标签
8
总字数
9,034
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录