JS 转 Go:入门与语法
本页关键词:静态类型、编译型、强类型、JIT、
go run、:=、零值、多返回值、range、无提升 浓缩自 langshift.dev js2go,面向 JS 开发者。
一、Go 是什么
Go(Golang)由 Google 在 2007 年设计、2009 年开源,目标是大规模服务端:编译快、语法少、并发是一等公民。
设计取向:
- 简洁:关键字少,默认靠约定而不是魔法
- 并发:goroutine + channel 内置
- 工具链:
gofmt/go test/go mod都在发行版里 - 部署:编成单一二进制,适合容器
- 内存:自动垃圾回收,不用手动申请/释放
Go 和 JavaScript 一样有 GC(垃圾回收):一块内存没有引用指向它之后,运行时会自动收回,不需要像 C 那样写 malloc / free。编译成机器码并不等于「没有 GC」——Rust 靠所有权在编译期管理内存,Go 没有走那条路。
Go 的 GC 是并发的:业务代码还在跑的同时可以扫堆,STW(Stop-The-World,全体暂停)通常很短,所以延迟比早期 Java 那种长时间卡顿稳得多。但它不是零开销:分配越勤,GC 占用的 CPU 越多,尾延迟也会上去。热路径里少造短命对象,这点和 Node 一样。
面试要点:Go 有 GC、不是手动内存管理;卖点是并发 GC 暂停短,不是「没有回收成本」。
二、五个基础概念
原笔记里的空问答补全如下。
静态类型 vs 动态类型
- 静态(Go):变量类型在编译期确定,
x = "a"赋给int会编不过。 - 动态(JS):类型跟值走,运行到那一行才知道。
编译型 vs 解释型
- 编译型:先变成机器码再执行,产物是可执行文件。
- 解释型:边读边执行。JS 属于「解释 + JIT」,不会事先产出独立二进制。
分布式系统 vs 云计算
- 分布式:多台机器协作完成一件事(拆服务、复制数据、用网络通信)。
- 云计算:按需租 CPU/内存/网络,不必自建机房。分布式系统经常跑在云上,但两者不是同一概念。
强类型 vs 弱类型
- 强类型:不允许稀里糊涂的隐式转换。Go 里
"1" + 1编译失败。 - 弱类型:运算时自动转。JS 里
"1" + 1 === "11"。 - 别和静/动态搞混:TypeScript 是静态但相对弱/可逃逸;Go 是静态且强。
JIT(即时编译)
JIT 的全称是 Just-In-Time compilation,意思是「运行时再编译」。JS 引擎不会像 Go 那样提前产出一个独立可执行文件,而是先把代码交给运行时执行;当 V8 发现某个循环、函数或代码路径反复执行,就会把这部分热点代码优化成更接近机器码的形式。
所以 Node 不是简单的「逐行解释,很慢」。它的执行过程大致可以理解成:
JS 源码 -> V8 解析和执行 -> 热点代码被 JIT 优化 -> 继续运行Go 的模型不同,常见链路是:
Go 源码 -> go build 提前编译 -> 得到二进制文件 -> 直接运行这叫 AOT(Ahead-Of-Time compilation,运行前编译)。它的特点是启动和部署更像传统后端程序:发布时带一个编译好的二进制,不要求目标机器安装 Go;而 Node 项目通常要带源码、依赖和 Node runtime。
比较 Go 和 Node 谁快时,不要只背一句「Go 更快」。更合理的拆法是:
- CPU 密集:大量计算、压缩、加密、图片处理这类场景,Go 通常更稳定,因为静态类型、AOT 编译和多核并发更直接。
- IO 密集:查数据库、调接口、读写网络这类场景,两者都能扛,真正瓶颈常在数据库、缓存、网络和业务设计。
- 启动和部署:Go 编译成单文件二进制,容器镜像和部署模型更轻;Node 要依赖运行时和包安装。
面试要点:被问「Go 和 Node 谁快」时先拆维度:CPU 密集 Go 通常更稳;IO 密集两者都能扛,瓶颈往往在业务和存储,不在语言口号。
三、和 JavaScript 的对照
| 特性 | JavaScript | Go |
|---|---|---|
| 类型 | 动态、弱类型 | 静态、强类型 |
| 执行 | 解释 + JIT | 先编译再执行 |
| 内存 | GC | GC |
| 并发 | 事件循环、Promise | goroutine、channel |
| 空值 | null / undefined | 统一 nil + 类型零值 |
| 部署 | 依赖 Node runtime | 单一二进制 |
Go 编译链路:解析 AST → 类型检查 → 生成机器码 → 链接。编译通常很快,开发时体感接近脚本语言。
常见去处:API / 微服务、云原生组件、CLI 与 DevOps 工具、网关和代理。Kubernetes、Docker、etcd 都是 Go 写的,这是生态事实,不是营销。
四、环境与工具链
mkdir hello && cd hello
go mod init example.com/hello # 生成 go.mod最小程序:
package main
import "fmt"
func main() {
fmt.Println("Hello, World from Go!")
}可执行程序必须是 package main,入口必须是 func main()。
| 命令 | 作用 | JS 类比 |
|---|---|---|
go run . | 编译当前包并运行 | node index.js,但会先编译 |
go run hello.go | 只编这一个文件 | 多文件项目容易漏同包文件 |
go build | 产出二进制 | 没有直接类比 |
go test | 跑 *_test.go | jest / vitest |
go fmt / gofmt | 格式化,团队无争议 | prettier(但是强制) |
go mod tidy | 按 import 对齐依赖 | 有点像清 package.json |
go vet | 静态检查常见坑 | eslint 的一个子集 |
开发用 go run .,不要用 go run main.go 跑多文件包。这里的 . 表示「当前目录这个包」,Go 会把当前目录下同一个 package 的 .go 文件一起编译再运行。
go run main.go 只把 main.go 这个文件交给编译器。如果当前目录还有 router.go、handler.go、config.go 这些同包文件,里面定义的函数可能就找不到,报 undefined: xxx。所以多文件项目里优先记住:
go run .发布用 go build。它不会直接启动程序,而是产出一个可执行文件:
go build
./hello这个文件已经是编译产物,部署到同系统同架构的机器上可以直接跑,不需要目标机器再装 Go。Node 项目更像是把源码和 node_modules 放过去,再用 node 启动。
模块文件长这样:
module example.com/hello
go 1.22
require github.com/gin-gonic/gin v1.9.1这就是 go.mod,可以先把它理解成 Go 项目的「模块身份证 + 依赖清单」:
module example.com/hello:当前项目的模块路径。别人 import 你的包时,会用这个路径作为前缀。go 1.22:这个模块声明使用的 Go 语言版本。require github.com/gin-gonic/gin v1.9.1:依赖gin这个模块,版本是v1.9.1。
它和 package.json 有点像,但不是完全等价。Go 的依赖路径通常就是仓库路径,没有 npm 那种短包名注册表。更完整的模块、包和 import 关系见 02-类型接口与包。
五、变量、控制流
声明
| 写法 | Go | 说明 |
|---|---|---|
| 短声明(冒等) | x := 10 | 只能在函数内,类型推断 |
| 显式 | var x int = 10 | 包级或函数内都可以 |
| 只声明 | var x int | 得到零值,不是 undefined |
| 常量 | const n = 10 | 编译期常量 |
| 多值 | a, b := 1, 2 | 很常见,尤其是 v, err := ... |
零值的意思是:变量只声明、不手动赋值时,Go 会自动给它一个默认值,不会像 JS 那样得到 undefined。
var age int
var name string
var active bool
fmt.Println(age) // 0
fmt.Println(name) // ""
fmt.Println(active) // false常见零值要记住:
| 类型 | 零值 |
|---|---|
| 数字类型 | 0 |
| 字符串 | "" |
| 布尔值 | false |
| 指针、切片、map、channel、接口、函数 | nil |
| 结构体 | 每个字段分别取自己的零值 |
所以 var u User 不是空对象,也不是 undefined,而是一个 User 值,里面每个字段都是零值。零值是 Go 里很重要的设计:很多类型声明出来就能用,但也要注意 nil map 不能直接写入,后面会讲。
Go 没有 JS 那种 var 提升。必须先声明再使用。if 条件必须是 bool,if (temperature) 这种真值判断编不过。
age := 30
name := "Alice"
scores := []int{85, 92, 78}
if temperature > 20 {
fmt.Println("暖")
}map[string]interface{} 里的 interface{}(Go 1.18 起别名 any)表示「这个值可以是任意类型」。JS 对象字段类型本来就随意;Go 的 map 值类型必须写死,想学 JS 对象就用 map[string]any,取出时再断言。详见下一篇接口。
循环:只有 for
for i := 0; i < 5; i++ { /* 传统 */ }
for i, v := range arr { /* 索引 + 值,类似 for...of 但带下标 */ }
for k, v := range m { /* 遍历 map */ }
for _, v := range arr { /* 只要值 */ }
for { /* 死循环,用 break / return 出去 */ }没有 while。range 切片/数组得到的是副本;改 v 不会改原元素。Go 1.22 起每次循环的 i/v 是新变量,以前在闭包里捕获循环变量会踩坑。
六、函数
func greet(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除零")
}
return a / b, nil
}
func sum(nums ...int) int { // 可变参数,类似 ...rest
total := 0
for _, n := range nums {
total += n
}
return total
}
add := func(a, b int) int { return a + b } // 函数值 / 闭包fmt.Sprintf 和 JS 里的模板字符串有点像,但它用的是格式化占位符。%s 表示这里要放一个字符串:
name := "Ada"
msg := fmt.Sprintf("Hello, %s!", name)
// msg == "Hello, Ada!"常见占位符:
| 占位符 | 含义 | 示例 |
|---|---|---|
%s | 字符串 | fmt.Sprintf("hi %s", "Ada") |
%d | 十进制整数 | fmt.Sprintf("%d", 42) |
%f | 浮点数 | fmt.Sprintf("%f", 3.14) |
%.2f | 保留 2 位小数 | fmt.Sprintf("%.2f", 3.14159) |
%v | 通用值,最常用 | fmt.Sprintf("%v", user) |
%+v | 结构体带字段名 | fmt.Sprintf("%+v", user) |
%#v | Go 语法形式 | fmt.Sprintf("%#v", user) |
%T | 值的类型 | fmt.Sprintf("%T", user) |
%q | 带引号的字符串 | fmt.Sprintf("%q", "hi") |
%% | 输出百分号 | fmt.Sprintf("90%%") |
日常打印调试可以多用 %v、%+v、%T;拼业务字符串时再按类型选 %s、%d、%.2f。
和 JS 的关键差:
- 参数和返回值都要写类型。
func greet(name string) string读作:函数名是greet,参数name的类型是string,返回值也是string。 - 多返回值是常态。
func divide(a, b float64) (float64, error)读作:返回两个值,第一个是结果,第二个是错误。 - 没有默认参数。JS 可以写
function f(page = 1) {},Go 不支持这种写法,通常用多个函数、配置结构体或 option 模式代替。 - 命名返回值可以裸
return,但可读性一般。面试知道即可,项目里优先显式return result, err。
Go 的错误处理和 JS 差异很大。JS 常用 throw / try...catch,错误会打断当前控制流:
try {
const result = divide(a, b);
} catch (err) {
console.error(err);
}Go 更常见的写法是把错误作为普通返回值交给调用方:
result, err := divide(a, b)
if err != nil {
return err
}
fmt.Println(result)这里的 result, err := ... 不是解构对象,而是接收函数返回的两个值。Go 的习惯是:函数如果可能失败,最后一个返回值通常写成 error;调用方拿到以后立刻判断 if err != nil。
要注意,编译器不会因为你没检查 err 就报错。下面这样能编译,但项目里一般不接受:
result, _ := divide(a, b) // _ 表示我故意丢掉这个返回值
fmt.Println(result)go vet、lint 和 code review 会重点看这类错误处理是否被忽略。
面试要点:JS 用 throw 打断控制流;Go 把失败当成第二个返回值。调用方必须写
if err != nil,编译器不管你忘没忘,但go vet和 code review 会管。
七、作用域与运算符
都有块级作用域,闭包都能用。Go 没有提升。内层 := 会造新变量,可能把外层同名变量遮住(见 06-陷阱与惯用法)。
func createCounter() func() int {
count := 0
return func() int {
count++
return count
}
}运算符大体和 JS 一样,注意:
- 没有
===/==那套宽松相等,==就是类型匹配下的比较。 - 没有三元
? :,用if。 - 整数除法截断:
3 / 2 == 1。要小数就用float。 - 字符串用
+拼接;重复拼接走strings.Builder。
类型细节(切片、map、结构体、接口、指针)放到下一篇,避免和语法课重复。