Easton Notes
知识八股GoJS 转 Go

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 的对照

特性JavaScriptGo
类型动态、弱类型静态、强类型
执行解释 + JIT先编译再执行
内存GCGC
并发事件循环、Promisegoroutine、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.gojest / 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.gohandler.goconfig.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 条件必须是 boolif (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 出去 */ }

没有 whilerange 切片/数组得到的是副本;改 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)
%#vGo 语法形式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、结构体、接口、指针)放到下一篇,避免和语法课重复。

On this page