JS 转 Go:类型、接口与包
本页关键词:零值、切片、map、struct、隐式接口、
any、类型断言、指针接收者、大小写可见性、go.mod、internal
一、静态类型在干什么
JS 的值可以换类型;Go 不行。好处是很多错误编译期就爆,代价是要学会切片、接口、指针这套词汇。
常用类型对照:
| JS | Go | 零值 |
|---|---|---|
number | int / int64 / float64 等 | 0 / 0.0 |
string | string(不可变,按字节 len) | "" |
boolean | bool | false |
null / undefined | nil(只给引用类型) | — |
Array | [n]T 数组(定长)或 []T 切片 | 数组全零;切片 nil |
Object / Map | struct / map[K]V | 结构体字段零值;map nil |
| 任意值 | any(即 interface{}) | nil |
- 切片就是动态数组罢了
日常几乎只用 int、int64、float64。需要固定宽度再写 int32/uint8(例如协议、二进制)。
字符串是只读字节序列。中文按 rune 遍历:for _, r := range s。len(s) 是字节数,不是字符数。
二、复合类型
数组 vs 切片
先读语法:
[3]int:长度为 3 的int数组。[4]int:长度为 4 的int数组。[]int:int切片,长度不写死,可以增长。
Go 里的数组长度是类型的一部分,所以 [3]int 和 [4]int 不是同一种类型。它们不能直接互相赋值:
var a [3]int
var b [4]int
a = b // 编译失败:类型不同这点和 JS 数组很不一样。JS 的数组长度是运行时属性,[1, 2, 3] 和 [1, 2, 3, 4] 都是 Array。Go 的数组更像「固定长度的一块值」,长度写进类型里。
日常业务代码几乎总是用切片 []T,因为它更接近 JS 数组:可以 append,长度可以变。
s := []int{1, 2, 3}
s = append(s, 4) // 必须接返回值
sub := s[1:3] // 和底层数组共享,改 sub 可能改 s
a := [3]int{1, 2, 3}
s2 := a[:] // 数组转切片这里要分清:
s := []int{1, 2, 3}创建的是切片。a := [3]int{1, 2, 3}创建的是数组。s2 := a[:]是从数组a上切出一个切片。
切片本身可以理解成一个很小的描述头,里面有三个信息:指针、len、cap。
- 指针:指向底层数组的某个位置。
len:当前能看到几个元素。cap:从当前位置开始,底层数组最多还能容纳几个元素。
append 容量不够会新分配数组,所以必须接住返回值:
s = append(s, 4)两个切片如果共享同一个底层数组,改其中一个可能影响另一个:
s := []int{1, 2, 3}
sub := s[1:3]
sub[0] = 99
fmt.Println(s) // [1 99 3]初学阶段先记住两句就够:固定长度用数组 [n]T,可变列表用切片 []T;业务代码默认选切片。
make([]int, 0, 16) 预分配 cap,减少扩容。不要用 == 比较切片,只能和 nil 比,或用 slices.Equal。
map
m := make(map[string]int)
m["a"] = 1
v, ok := m["a"] // 逗号 ok,区分「没这个键」和「值就是零值」
delete(m, "a")m := make(map[string]int) 读作:创建一个 map,键是 string,值是 int。它接近 JS 里的对象或 Map,但键和值的类型必须提前写清楚。
v, ok := m["a"] 不是 JS 的解构赋值。它是 Go 的多返回值接收。读取 map 时可以拿到两个结果:
v:这个 key 对应的值。ok:这个 key 是否真的存在。
为什么需要 ok?因为 map 读不到 key 时,会返回值类型的零值:
m := map[string]int{"a": 0}
v1, ok1 := m["a"] // v1 是 0,ok1 是 true:key 存在,值就是 0
v2, ok2 := m["b"] // v2 也是 0,ok2 是 false:key 不存在如果只写 v := m["b"],你只能拿到 0,分不清「真的存了 0」还是「没有这个 key」。所以 Go 里查 map 时常见写法是:
if v, ok := m["a"]; ok {
fmt.Println(v)
}delete(m, "a") 是删除 key。key 不存在也不会报错。
未 make 的 map 是 nil map:
var m map[string]int
fmt.Println(m["a"]) // 0,可以读,读不到就返回零值
m["a"] = 1 // panic:assignment to entry in nil map所以要写入 map,先用 make 或字面量初始化:
m1 := make(map[string]int)
m2 := map[string]int{"a": 1}另外两个常见规则:
- 遍历 map 的顺序每次都可能变,不要依赖它。
- 键必须可比较,不能用切片当键;字符串、数字、布尔值可以当键。
结构体
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
u := User{Name: "Ada", Age: 30}这段是在定义一个结构体类型。可以逐行读:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}type User struct:定义一个新类型,名字叫User,它是一个结构体。Name string:字段名是Name,类型是string。Age int:字段名是Age,类型是int。`json:"name"`:结构体标签,告诉 JSON 编码/解码时这个字段对应 JSON 里的name。`json:"age"`:同理,对应 JSON 里的age。
为什么 Go 字段是 Name,JSON 却是 name?因为 Go 用首字母大小写控制可见性:Name 大写,包外可见,JSON 包才能读到;但接口返回给前端时通常希望字段是小写的 name,所以用 tag 做映射。
创建结构体值时推荐写字段名:
u := User{Name: "Ada", Age: 30}这比按位置写更清楚,也不怕以后字段顺序调整。Go 也支持按位置初始化:
u := User{"Ada", 30}但项目里少用,因为字段一多就很难读。
Go 没有 class / 继承,复用靠嵌入:
type Person struct{ Name string }
func (p Person) Greet() string { return "hi " + p.Name }
type Employee struct {
Person
Salary int
}
// emp.Greet()、emp.Name 可直接用,等于组合而不是 IS-A面试要点:JSON 只导出大写字段。
name string序列化会丢;要小写键名用 tag:`json:"name"`。
三、接口
Go 接口是方法集合,满足是隐式的:类型不用声明 implements,有方法就算。
type Measurable interface {
Area() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func printArea(m Measurable) { fmt.Println(m.Area()) }
printArea(Circle{Radius: 5}) // Circle 没写 implements接口可嵌套:type ReadWriter interface { Reader; Writer }。标准库 io.Reader / http.Handler 都是小接口。
any(interface{})能装任何值,等于放弃静态信息,取出要用断言:
var v any = "hello"
s := v.(string) // 失败则 panic
s, ok := v.(string) // 推荐
switch x := v.(type) { // 类型 switch
case string:
_ = x
case int:
_ = x
}原笔记问过:map[string]interface{} 的 interface 是干什么的?就是「值的类型到运行时再定」,接近 JS 对象。能用具体类型就不要用 any。
面试要点:接口值内部是
(动态类型, 动态值)。把nil指针赋给接口后,接口不是 nil(类型还在)。这是if err != nil的经典坑,见第 6 篇。
四、指针
JS 对象默认引用传递;Go 默认拷贝值。要改调用方的结构体,用指针。
n := 42
p := &n // *int,指向 n
*p = 100 // n 变成 100
func scale(r *Rect, k int) { r.W *= k }
func (r *Rect) Scale(k int) { r.W *= k } // 指针接收者,可改接收者
func (r Rect) Area() int { return r.W * r.H } // 值接收者,改的是副本规则:
- 要改接收者,或结构体较大,用指针接收者。
- 同一类型的方法接收者尽量统一,不要有的
T有的*T。 - 零值指针是
nil,解引用*p会 panic,先判空。 - Go 有 GC,没有
free,也没有指针运算。
方法集:*T 的方法,值 T 也能调(编译器取地址);反过来,若只有值接收者,接口变量里若是指针也能调。面试常问「为什么接口装值/指针有时编不过」:看方法集是否包含接口要求的方法。
五、包与可见性
同一目录所有 .go 必须同一个 package 名。导入的是包,不是文件。
package utils
var Public = 1 // 包外可见
var hidden = 2 // 仅包内首字母大写 = 导出(类似 export),小写 = 包私有。结构体字段同样。没有 export default。
特殊目录:
| 目录 | 含义 |
|---|---|
internal/ | 只有 internal 的父树能 import,编译器强制 |
cmd/xxx/ | 可执行入口,一个子目录一个二进制 |
pkg/ | 社区约定的对外库,不是关键字 |
导入:
import (
"fmt"
"github.com/gin-gonic/gin"
myfmt "fmt" // 别名
_ "github.com/lib/pq" // 只跑 init,注册驱动
)点导入 . "fmt" 会污染命名空间,生产代码别用。循环 import 直接编译失败,用接口解耦或抽第三包。
六、模块:go.mod 对照 npm
| npm | Go | |
|---|---|---|
| 清单 | package.json | go.mod |
| 锁 | package-lock.json | go.sum(哈希) |
| 包名 | express | github.com/gin-gonic/gin |
| 安装位置 | node_modules/ | 模块缓存 $GOPATH/pkg/mod |
| 加依赖 | npm i express | go get github.com/gin-gonic/gin |
| 清理 | 手工 | go mod tidy |
go mod init github.com/you/app
go get github.com/gin-gonic/gin@v1.9.1
go mod tidy
go list -m all模块路径就是 import 路径,clone 下来就能编译,不经过中央注册表(proxy 只是缓存)。版本用 Git tag,如 v1.2.3。
go work 用来本地同时开发多个 module,相当于简单 monorepo,了解即可。vendor/ 是把依赖拷进仓库,CI 离线构建才需要。
面试要点:导出靠大小写不是风格问题,是语言规则;JSON、跨包调用、测试里的
Foo/foo全跟它挂钩。