Go 语言基础笔记 --- 面向 JS/TS 前端开发者
基础部分:程序结构、变量、数据类型、运算符、流程控制、数组、切片、Map、指针、函数、字符串处理、类型转换、错误处理、defer/panic/recover
1. 程序结构与包管理
Go 文件基本骨架
go
// package: 声明包名,每个 Go 文件必须属于一个包
// main 包是特殊包:一个可执行程序有且只有一个 main 包
package main
// import: 导入其他包
import "fmt"
// main 函数是整个程序的入口,没有 main 函数则程序无法执行
func main() {
fmt.Println("Hello World, Hello Go")
}
JS/TS 等效对比
js
// JS 没有强制包声明,一个 .js 文件天然就是独立模块
console.log("Hello World")
ts
// TS 通过 import/export 管理模块
export const hello = () => console.log("Hello World")
| 对比项 | Go | JS/TS |
|---|---|---|
| 包声明 | package main 必须显式声明 |
无,文件即模块 |
| 入口函数 | func main() 约定入口 |
无,代码从上到下执行 |
| 导入模块 | import "fmt" |
import { fn } from 'module' |
| 可执行程序 | 必须有 package main + func main() |
任意文件都可以执行 |
Go 独有特性
- main 包和 main 函数缺一不可,否则编译报错
- import 未使用的包会编译报错(Go 强制清洁代码)
- 花括号风格必须 是
func main() {------ 左括号不能换行
易错点
go
// ❌ 错误:左花括号不能换行
func main()
{
// syntax error
}
// ✅ 正确:左花括号必须跟在函数名同行
func main() {
// ok
}
2. 变量与常量
Go 变量声明三种方式
go
package main
import (
"fmt"
"reflect"
)
func main() {
// 方式一:var 声明,显式指定类型
var name string = "杜宽"
fmt.Println("我的名字是:", name)
// 声明后再赋值
var name2 string
name2 = "dot"
// 方式二:批量声明
var (
name3 string = "name3"
name4 int
)
// 方式三:语法糖 :=(自动推断类型,最常用)
b := true // bool
f := 3.1415926 // float64
fmt.Println(b, f)
// 查看变量类型
fmt.Println("name2的类型是:", reflect.TypeOf(name2)) // string
fmt.Println("f的类型是:", reflect.TypeOf(f)) // float64
}
JS/TS 等效对比
js
// JS --- 动态类型
let name = "杜宽"
let name2
name2 = "dot"
let b = true
let f = 3.1415926
console.log(typeof name2) // "string"
ts
// TS --- 显式类型注解
let name: string = "杜宽"
let name2: string
name2 = "dot"
let b: boolean = true
let f: number = 3.1415926
| 对比项 | Go | JS/TS |
|---|---|---|
| 声明+赋值 | var x string = "hi" |
let x: string = "hi" |
| 类型推断 | x := "hi"(仅函数内可用) |
let x = "hi" |
| 未赋值默认值 | 有零值("" / 0 / false) |
undefined |
| 类型反射 | reflect.TypeOf(x) |
typeof x |
Go 独有特性
- 零值机制 :Go 中变量声明后一定有值(零值),不会出现
undefinedstring默认""int默认0bool默认false- 指针/slice/map 默认
nil
:=只能在函数内部使用 ,包级别(函数外)只能用var
go
// ✅ 包级别
var globalName string = "Dotbalo"
// ❌ 包级别不能用 :=
// globalName2 := "error"
// ✅ 函数内可以用 :=
func main() {
name := "ok"
}
常量
go
// Go 常量
const c1, c2, c3 = 1, 2, 3
// const c1 = 5 // ❌ 常量不可修改
// 批量常量
const (
pi = 3.14159
maxNum = 100
)
ts
// TS 常量
const c1 = 1
const c2 = 2
核心差异 :Go 的 const 是编译时常量,值必须在编译期确定;JS 的 const 是运行时常量引用,对象属性仍可变。
3. 数据类型全解
整数类型
| Go 类型 | 范围 | JS/TS 对应 |
|---|---|---|
int |
32位或64位(取决于系统) | number |
int8 |
-128 ~ 127 | Java 的 byte |
int16 |
-32768 ~ 32767 | --- |
int32 / rune |
-2^31 ~ 2^31-1 | --- |
int64 |
-2^63 ~ 2^63-1 | bigint |
uint |
无符号,0 ~ MaxUint | --- |
uint8 / byte |
0 ~ 255 | --- |
go
package main
import (
"fmt"
"math"
"reflect"
)
func main() {
// 不指定类型的整数字面量默认是 int
defaultIntType := 1
fmt.Println("默认的数值类型是:", reflect.TypeOf(defaultIntType)) // int
// 显式指定
var int64Num int64 = 1
var uintNum uint = 1
fmt.Println("int的取值范围:", math.MinInt, math.MaxInt)
// ❌ 不同类型不能直接运算
// var n1 int = 1
// var n2 int64 = 2
// sum := n1 + n2 // 编译报错!
}
浮点类型
go
var floatNum1 float64 = 3.14
var floatNum2 float32 = 3.15
// floatSum := floatNum1 + floatNum2 // ❌ 不同浮点类型也不能直接运算
布尔类型
go
b := true
b2 := false
b = b2 // ✅ 可以重新赋值
Go ↔ JS/TS 类型对比总结
go
// Go:强类型,不同数字类型不能隐式转换
var a int = 1
var b int64 = 2
// c := a + b // ❌ 编译错误
c := a + int(b) // ✅ 必须显式转换
ts
// TS:数字就是 number,范围由 IEEE 754 决定
let a: number = 1
let b: number = 2
let c = a + b // ✅ 直接运算
核心差异 :Go 是强静态类型语言。int 和 int64 是不同的类型,必须显式转换才能一起运算。JS/TS 中所有数字都是 number(64位浮点),没有细分类型。
4. 运算符
关系和逻辑运算符
go
package main
import "fmt"
func main() {
// 比较运算符(和 JS 一样)
fmt.Println(727585 > 727588) // false
fmt.Println(727585 < 727588) // true
fmt.Println("a" == "b") // false
fmt.Println(3.14 == 3.14) // true
// 字符串比较(按 ASCII 逐字符比,注意:Go 不可以像 JS 用 ===)
s1 := "dukuan"
s2 := "dotbalo"
fmt.Println("s1和s2相等: ", s1 == s2) // false
fmt.Println("s1和s2不相等: ", s1 != s2) // true
fmt.Println("s1 > s2:", s1 > s2) // true (d>d, u>o)
fmt.Println("s2 > s1:", s2 > s1) // false
// 逻辑运算符(和 JS 一样)
n1 := 1
n2 := 1
n3 := 2
fmt.Println(n1 == n2 && n2 == n3) // false
fmt.Println(n1 == n2 || n2 == n3) // true
}
| 运算符 | Go | JS/TS | 说明 |
|---|---|---|---|
| 等于 | == |
== / === |
Go 没有 ===,因为不存在类型转换 |
| 不等于 | != |
!= / !== |
同上 |
| 与 | && |
&& |
相同的短路逻辑 |
| 或 | ` | ` | |
| 非 | ! |
! |
相同 |
自增/自减
go
// Go:++ 和 -- 是语句,不是表达式
p1 := 8
p1++ // ✅ 正确
p1-- // ✅ 正确
// p2 := p1++ // ❌ 编译错误!不能像 JS 那样在表达式中使用
js
// JS:++ 和 -- 是运算符,可以用在表达式中
let p1 = 8
p1++
let p2 = p1++ // ✅ JS 中可以
Go 独有 :++ / -- 是语句不是表达式。不能写成 x := y++,也不能在 for 条件中当表达式用(但 for 的 post 语句中可以用)。
5. 流程控制
if-else
go
package main
import "fmt"
// 雨伞价格函数
func printPrice(weather string) {
defaultPrice := 10
if weather == "sunny" {
fmt.Println("今天是晴天,雨伞的价格是:", defaultPrice)
} else {
fmt.Println("今天不是晴天,雨伞的价格是:", defaultPrice+10)
}
}
// if - else if - else 链
func printPriceWithWeather(weather string) {
defaultPrice := 10
if weather == "lightRain" {
fmt.Println("下小雨了,雨伞的价格是:", defaultPrice+5)
} else if weather == "heavyRain" {
fmt.Println("下大雨了,雨伞的价格是:", defaultPrice+10)
} else if weather == "rainStorm" {
fmt.Println("下暴雨了,雨伞的价格是:", defaultPrice+20)
} else {
fmt.Println("雨伞的价格是:", defaultPrice)
}
}
Go ↔ JS 异同:
| 对比项 | Go | JS/TS |
|---|---|---|
| 条件括号 | if x > 1 { 不需要括号 |
if (x > 1) { 需要括号 |
| 花括号 | 必须加,不能省略 | 单行可省略(但不推荐) |
| 赋值+判断 | if err := fn(); err != nil { |
不能,需分两行 |
| 三元运算符 | 没有 ? : |
x > 1 ? a : b |
Go 独有 --- if 中声明变量:
go
// Go 可以在 if 条件前执行一个简短声明
if err := someFunction(); err != nil {
// 处理错误
}
// err 的作用域仅限于 if-else 块
switch
go
package main
import "fmt"
func printPriceWithSwitch(weather string) {
defaultPrice := 10
switch weather {
case "lightRain":
fmt.Println("下小雨了,雨伞的价格是:", defaultPrice+5)
case "heavyRain":
fmt.Println("下大雨了,雨伞的价格是:", defaultPrice+10)
case "rainStorm":
fmt.Println("下暴雨了,雨伞的价格是:", defaultPrice+20)
case "snowing", "sunny": // 多个值匹配同一个分支
fmt.Println("雨伞的价格是:", defaultPrice)
default:
fmt.Println("我不知道现在是什么天气,所以我不卖了~")
}
}
Go ↔ JS 的 switch 核心差异:
| 对比项 | Go | JS/TS |
|---|---|---|
| break | 默认自带,不写也不会穿透 | 需要显式 break 否则会穿透 |
| fallthrough | 需要穿透时显式写 fallthrough |
不需要 |
| 多值匹配 | case "a", "b": |
case "a": case "b": 或 case "a", "b": |
| 表达式 | switch x { 也可以 switch { 无表达式 |
必须有表达式 |
for 循环
Go 只有 for 一种循环关键字(没有 while、do-while),但 for 可以充当三种角色:
go
package main
import "fmt"
func main() {
// 形式一:传统 for 循环(三段式)
count := 0
for num := 1; num <= 100; num++ {
if num%2 == 0 {
fmt.Println("发现一个偶数:", num)
count++
}
}
fmt.Printf("1-100一共有偶数:%d个\n", count)
// 形式二:for 作为 while 使用(只有条件)
num2 := 1
for num2 <= 100 {
if num2%2 != 0 {
fmt.Println("发现奇数:", num2)
}
num2++
}
// 形式三:无限循环(相当于 while(true))
// for {
// 这里面的代码会一直执行
// }
}
JS/TS 等效:
ts
// JS 三段式 for
let count = 0
for (let num = 1; num <= 100; num++) {
if (num % 2 === 0) {
console.log("发现一个偶数:", num)
count++
}
}
// JS while
let num2 = 1
while (num2 <= 100) {
if (num2 % 2 !== 0) {
console.log("发现奇数:", num2)
}
num2++
}
// JS 无限循环
// while (true) { }
break 和 continue
go
// break:终止整个循环
for i := 0; i < 100; i++ {
if i == 88 {
fmt.Println("我找到了88")
break // 循环到这里结束
}
fmt.Println("现在的数值是:", i)
}
// continue:跳过本次迭代,继续下一次
for i := 0; i < 100; i++ {
if i == 88 {
fmt.Println("我找到了88")
continue // 跳过打印,继续下一次循环
}
fmt.Println("现在的数值是:", i)
}
Go 和 JS 在 break/continue 上用法完全相同。
range --- Go 的数据遍历利器
Go 的 range 用于遍历数组、切片、map、字符串,类似 JS 的 for...of + 自动索引。
go
// range 返回 (索引, 值)
teacherAgeArray := [3]int{18, 19, 20}
for k, v := range teacherAgeArray {
fmt.Printf("第%d位老师的年龄是: %d\n", k+1, v)
}
// 只要索引
for k := range teacherAgeArray {
fmt.Println(k)
}
// 只要值(用 _ 忽略索引)
for _, v := range teacherAgeArray {
fmt.Println(v)
}
ts
// JS 等效
const ages = [18, 19, 20]
for (const [k, v] of ages.entries()) {
console.log(`第${k + 1}位老师的年龄是: ${v}`)
}
Go 独有 :_(下划线)是空白标识符,用于忽略不需要的返回值。如果声明了变量但不用,Go 编译报错,所以需要 _ 来丢弃。
6. 数组
Go 数组
go
package main
import "fmt"
func main() {
// 数组:一组具有相同类型且长度固定的数据集合
var teacherNameArray = [3]string{"杜宽", "Dot", "嘚嘚"}
teacherAgeArray := [3]int{18, 19, 20}
// 访问元素:数组名[下标],下标从0开始
fmt.Println("第一位老师的名字是:", teacherNameArray[0]) // 杜宽
// 修改元素
teacherNameArray[2] = "dotbalo"
// ❌ 数组长度不可变
// teacherNameArray[3] = "数据4" // 编译错误!index out of range
fmt.Println("数组的长度是:", len(teacherNameArray)) // 3
// 遍历方法一:传统 for
for i := 0; i < len(teacherNameArray); i++ {
fmt.Printf("第%d个数据为: %s\n", i+1, teacherNameArray[i])
}
// 遍历方法二:range(推荐)
for k, v := range teacherAgeArray {
fmt.Printf("第%d位老师的年龄是: %d\n", k+1, v)
}
// 自动推断长度
array3 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println("array3的长度是:", len(array3)) // 9
}
JS/TS 等效对比
ts
// TS 数组
const teacherNames: string[] = ["杜宽", "Dot", "嘚嘚"]
const teacherAges: number[] = [18, 19, 20]
console.log(teacherNames[0]) // 杜宽
teacherNames[2] = "dotbalo" // 可以修改
teacherNames.push("数据4") // JS 数组可以动态添加
for (const [k, v] of teacherAges.entries()) {
console.log(`第${k + 1}位老师的年龄是: ${v}`)
}
| 对比项 | Go 数组 | JS 数组 |
|---|---|---|
| 长度 | 固定,编译后不可变 | 动态,可增删 |
| 类型 | 固定类型 [3]int |
可混合类型 |
| 值/引用 | 值类型,赋值会完整复制 | 引用类型,赋值共享同一数组 |
| 长度属性 | len(arr) 函数 |
arr.length 属性 |
Go 数组独有特性 --- 值类型:
go
a := [3]int{1, 2, 3}
b := a // b 是 a 的完整副本(值拷贝)
b[0] = 999
fmt.Println(a[0]) // 1 --- a 不受影响!
// 而 JS 中:
// let a = [1,2,3]; let b = a; b[0] = 999; console.log(a[0]) // 999 !
JS 开发者易错点:Go 数组作为函数参数时,传递的是整个数组的副本(值传递),修改副本不会影响原数组。如果数组很大,性能开销也大。通常用**切片(Slice)**代替。
7. 切片(Slice)
切片是 Go 中最常用的数据结构,可以理解为动态数组,类似 JS 的数组。Go 中 90% 的场景用切片而非数组。
切片声明和初始化
go
package main
import "fmt"
func main() {
// 方式一:var 声明(零值是 nil)
var s1 []int
fmt.Println("最初的切片数据:", s1) // []
fmt.Println("切片的默认长度是:", len(s1)) // 0
fmt.Println("切片的默认容量是:", cap(s1)) // 0
// 追加元素(append)
s1 = append(s1, 7275, 85266)
fmt.Println("长度是:", len(s1)) // 2
fmt.Println("容量是:", cap(s1)) // 2
fmt.Println("数据:", s1) // [7275 85266]
// 方式二:make 指定长度和容量
s2 := make([]int, 5, 10) // 长度5,容量10
fmt.Println("切片的默认长度是:", len(s2)) // 5
fmt.Println("切片的默认容量是:", cap(s2)) // 10
fmt.Println("最初的切片数据:", s2) // [0 0 0 0 0]
s2 = append(s2, 1, 2, 3, 4, 5, 6)
// 超过容量时,Go 自动扩容(约 2 倍增长)
fmt.Println("长度是:", len(s2)) // 11
fmt.Println("容量是:", cap(s2)) // 20(10*2)
// 修改元素(和数组一样)
s2[0] = 88
fmt.Println("切片的数据:", s2)
// range 遍历
for k, v := range s2 {
fmt.Printf("第%d个数据是: %d\n", k+1, v)
}
}
JS/TS 等效
ts
// TS:数组天然是动态的
let s1: number[] = []
console.log(s1.length) // 0
s1.push(7275, 85266)
console.log(s1.length) // 2
// 创建固定长度、预填充的数组
let s2: number[] = new Array(5).fill(0) // [0, 0, 0, 0, 0]
s2.push(1, 2, 3, 4, 5, 6)
s2[0] = 88
Go 切片关键概念:
len(slice)--- 当前元素个数cap(slice)--- 底层数组的容量- 当
append超出容量时,Go 自动分配新底层数组,容量大约翻倍 make([]T, length, capacity)--- 初始化切片的标准方式
切片截取
go
s := []int{12, 2312, 33, 314, 2345, 346, 7342, 438, 3439}
// 前4个元素(索引0到3)
fmt.Println("切片s前四位数据是:", s[:4]) // [12 2312 33 314]
fmt.Println("切片s前四位数据是:", s[0:4]) // [12 2312 33 314]
// 索引2到3(左闭右开区间)
fmt.Println("切片3-4位的数据:", s[2:4]) // [33 314]
// 从索引4到末尾
fmt.Println("切片从第5个开始的数据:", s[4:]) // [2345 346 7342 438 3439]
对应 JS 行为:
ts
const arr = [12, 2312, 33, 314, 2345, 346, 7342, 438, 3439]
arr.slice(0, 4) // [12, 2312, 33, 314]
arr.slice(4) // [2345, 346, 7342, 438, 3439]
关键差异 :Go 切片截取返回的是原数组的视图(引用共享底层数组) ,修改子切片会影响原切片!JS 的 slice() 返回的是浅拷贝新数组。
切片元素删除
Go 没有内置的删除方法,需要用切片操作拼接实现:
go
s := []int{12, 2312, 33, 314, 2345, 346, 7342, 438, 3439}
// 删除第一个元素
s = s[1:]
// 删除最后一个元素
s = s[:len(s)-1]
// 删除索引为2的元素(314)
// append(s[:2], s[3:]...) 将两部分拼接
// ... 是把切片展开为一个个元素(类似 JS 的 ...spread)
s = append(s[0:2], s[3:]...)
ts
// TS 等效
const arr = [12, 2312, 33, 314, 2345, 346, 7342, 438, 3439]
arr.shift() // 删第一个
arr.pop() // 删最后一个
arr.splice(2, 1) // 删索引2的元素
深拷贝与浅拷贝
go
package main
import (
"fmt"
"unsafe"
)
func main() {
// 示例1:string 是值类型,赋值就是复制
s1 := "dk"
s2 := s1
s2 = "dot"
fmt.Println("s1和s2:", s1, s2) // dk dot --- 互不影响
// 示例2:切片是引用类型,直接赋值共享底层数组(浅拷贝)
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1 // 浅拷贝!
slice2[1] = 88
fmt.Println("修改后的slice1:", slice1) // [1 88 3 4 5] --- 原切片也被改了!
// 示例3:深拷贝用 copy
slice3 := make([]int, len(slice1), cap(slice1))
copy(slice3, slice1) // 深拷贝!
slice3[1] = 99
fmt.Println("修改后的slice1:", slice1) // [1 88 3 4 5] --- 不受影响
fmt.Println("修改后的slice3:", slice3) // [1 99 3 4 5]
// 查看内存地址
fmt.Println("slice1的内存地址:", unsafe.Pointer(&slice1))
fmt.Println("slice3的内存地址:", unsafe.Pointer(&slice3))
}
JS/TS 等效:
ts
// string 是原始类型,赋值即复制
let s1 = "dk"
let s2 = s1
s2 = "dot"
console.log(s1, s2) // dk dot --- 互不影响
// 数组是引用类型,直接赋值共享(浅拷贝)
let arr1 = [1, 2, 3, 4, 5]
let arr2 = arr1
arr2[1] = 88
console.log(arr1) // [1, 88, 3, 4, 5] --- 也被改了!
// 深拷贝
let arr3 = [...arr1] // 展开运算符
// 或 let arr3 = structuredClone(arr1)
| 对比项 | Go | JS/TS |
|---|---|---|
| 值类型 | int, float, string, bool, array, struct | number, string, boolean, undefined, null, symbol, bigint |
| 引用类型 | slice, map, pointer, channel, interface | Object, Array, Function, Map, Set |
| 深拷贝 | copy(dst, src) |
[...arr] / structuredClone() |
| 展开操作符 | slice...(只在函数参数/append中使用) |
...arr(任意位置展开) |
8. Map(对象/字典)
Go 的 map 类似 JS 的 Object / Map,是一种 key-value 结构。
基本使用
go
package main
import "fmt"
func main() {
// 方式一:make 初始化
teacherAge := make(map[string]int)
fmt.Println("对象的初始化值:", teacherAge) // map[]
teacherAge["dukuan"] = 18
teacherAge["dot"] = 20
teacherAge["xiaoming"] = 21
teacherAge["dukuan"] = 21 // 后面的值会覆盖前面的值
fmt.Println("赋值后的值", teacherAge) // map[dot:20 dukuan:21 xiaoming:21]
// 方式二:声明时直接赋值
teacherAge2 := map[string]int{
"d1": 2,
"d2": 3,
}
fmt.Println("teacherAge2:", teacherAge2)
// ❌ 易错:var 声明的 nil map 不能直接赋值
var teacherAddress map[string]string
// teacherAddress["dukuan"] = "北京" // 运行时会 panic!
teacherAddress = make(map[string]string) // 必须先 make 初始化
teacherAddress["dukuan"] = "北京" // 现在可以了
teacherAddress["dot"] = "加利福尼亚"
}
ts
// TS 等效
let teacherAge: Record<string, number> = {}
teacherAge["dukuan"] = 18
teacherAge["dot"] = 20
teacherAge["xiaoming"] = 21
teacherAge["dukuan"] = 21 // 覆盖
// 声明时赋值
const teacherAge2: Record<string, number> = {
d1: 2,
d2: 3,
}
查询、修改、删除
go
// 查询
fmt.Println("查询dukuan老师的地址:", teacherAddress["dukuan"])
// 查询不存在的 key:返回零值(string 返回 ""),不会报错!
fmt.Println("取一个不存在的值:", teacherAddress["eeeeee"]) // ""
// 安全查询:comma ok 模式(判断 key 是否存在)
address, ok := teacherAddress["dukuan"]
if ok {
fmt.Println("能查看到dukuan:", address)
} else {
fmt.Println("不能查看到dukuan")
}
// 修改
teacherAddress["dukuan"] = "上海"
// 删除
delete(teacherAddress, "dukuan")
fmt.Println("删除后的map:", teacherAddress)
ts
// TS 等效
console.log(teacherAddress["dukuan"])
// TS 判断 key 是否存在
const address = teacherAddress["dukuan"]
if (address !== undefined) {
console.log("能查看到dukuan:", address)
}
// 修改
teacherAddress["dukuan"] = "上海"
// 删除
delete teacherAddress["dukuan"]
Go map 易错点:
var m map[K]V声明得到的是nil,不能直接赋值,必须用make初始化- 访问不存在的 key 返回零值,不报错
- 用
val, ok := m[key]的 "comma ok" 模式判断是否存在 delete(m, key)删除不存在的 key 也不会报错
数据嵌套
go
// 切片嵌套 map:每天的菜单
order1 := map[string]int{"宫保鸡丁": 45, "糖醋鱼": 80}
order2 := map[string]int{"糖醋里脊": 55, "回锅肉": 50}
order3 := map[string]int{"奶茶": 20, "啤酒": 10}
var menu []map[string]int
menu = append(menu, order1, order2, order3)
for k, v := range menu {
fmt.Printf("第%d天的菜单是:\n", k+1)
for name, price := range v {
fmt.Printf("\t菜:%s, 价格:%d\n", name, price)
}
}
// map 嵌套 map
// teacherInfo := make(map[string]map[string]string)
// teacherInfo["dukuan"] = map[string]string{"address": "北京", "phone": "138xxxx"}
// teacherInfo["dukuan"]["name"] // 访问嵌套值
ts
// TS 等效
interface MenuItem { [dish: string]: number }
const order1: MenuItem = { "宫保鸡丁": 45, "糖醋鱼": 80 }
const order2: MenuItem = { "糖醋里脊": 55, "回锅肉": 50 }
const order3: MenuItem = { "奶茶": 20, "啤酒": 10 }
const menu: MenuItem[] = [order1, order2, order3]
menu.forEach((day, k) => {
console.log(`第${k + 1}天的菜单是:`)
Object.entries(day).forEach(([name, price]) => {
console.log(`\t菜:${name}, 价格:${price}`)
})
})
9. 指针
概念:用前端思维理解指针
&x--- 取变量 x 的内存地址(好比获取这个变量的门牌号)*T--- 声明这是一个指向类型 T 的指针类型*p--- 通过指针 p 读取/修改它指向的值(拿着门牌号去开门)
Go 指针实操
go
package main
import "fmt"
// 值传递:修改不影响外部
func updateString(s string) {
s = "这是一个新值" // 只改了副本
}
// 指针传递:修改影响外部
func updateStringWithPointer(s *string) {
*s = "这是一个新值" // 通过指针修改原值
}
func main() {
var s string
s = "这是一个字符串"
// &s:取 s 的内存地址
fmt.Println("变量s的内存地址是:", &s)
// sp 是指向 string 的指针
sp := &s
fmt.Println("指针sp:", sp)
// 声明指针变量(零值是 nil)
var sp2 *string
fmt.Println("指针sp2:", sp2) // <nil>
sp2 = &s
fmt.Println("指针sp2:", sp2)
// 通过指针获取值:*指针变量
fmt.Println("指针对应内存地址的值:", *sp2)
// 值传递 --- 改不了
updateString(s)
fmt.Println("修改后的s:", s) // "这是一个字符串" --- 没变!
// 指针传递 --- 可以改
updateStringWithPointer(&s)
fmt.Println("真正修改后的s:", s) // "这是一个新值" --- 变了!
}
JS/TS 对应关系
ts
// JS/TS 没有指针概念,但可以这样类比理解:
// 原始类型(string, number, boolean)→ Go 值类型(传副本)
function updateString(s: string): void {
s = "新值" // 只改了局部变量,不影响外部
}
// 引用类型(Object, Array)→ Go 指针(传地址)
function updateObject(obj: { val: string }): void {
obj.val = "新值" // 改了原对象
}
// Go 的指针是对"值类型"实现引用传递的方式
// JS 中所有对象天生就是引用传递,原始值天生就是值传递
Go 指针 vs JS 引用的本质区别
| 对比项 | Go 指针 | JS 引用 |
|---|---|---|
| 概念 | 显式内存地址操作 | 隐式的引用机制 |
| 语法 | &x 取地址,*p 取值 |
自动处理 |
| 可对原始类型取地址 | ✅ var p *int = &num |
❌ 不能 |
| 指针运算 | ❌ Go 不支持指针算术 | ❌ |
| nil 指针 | ✅ var p *int 为零值 nil |
引用可为 null / undefined |
Go 独有:
- 指针是 Go 语言的一等公民,但不是 C 那种"危险的指针"------没有指针算术
- Go 通过指针实现函数间对值的修改,一定程度上替代了 JS 中的"对象引用"模式
nil指针调用方法会导致 panic,需要注意判空
10. 函数
函数定义语法
go
// func 函数名(参数名 类型, 参数名 类型) 返回值类型 { 函数体 }
// 基本函数:两个 int 参数,返回 int
func max(i1, i2 int) int {
if i1 > i2 {
return i1
}
return i2
}
// 命名返回值:返回值变量在函数签名中声明
func qiuhe(i1, i2 int) (sum int) {
sum = i1 + i2
return // 裸 return,自动返回 sum
}
// 多返回值(Go 特色!)
func paixu(i1, i2 int) (min, max int) {
if i1 > i2 {
max = i1
min = i2
} else {
max = i2
min = i1
}
return
}
// 不定长度参数(variadic function)
func hasName(s ...string) string {
// s 是一个 []string 切片
return strings.Join(s, "-")
}
func main() {
res := max(727585, 266)
fmt.Println("最大值:", res)
sum := qiuhe(7275, 85266)
fmt.Println("求和:", sum)
min, max := paixu(43, 34)
fmt.Println("从小到大排序:", min, max)
m := hasName("du", "kuan", "is")
fmt.Println("拼接后的字符串:", m) // du-kuan-is
}
JS/TS 等效对比
ts
// 基本函数
function max(i1: number, i2: number): number {
if (i1 > i2) return i1
return i2
}
// 多返回值:TS 需要用对象或数组模拟
function paixu(i1: number, i2: number): [number, number] {
if (i1 > i2) return [i2, i1] // [min, max]
return [i1, i2]
}
const [min, max] = paixu(43, 34)
// 不定参数
function hasName(...s: string[]): string {
return s.join("-")
}
| 对比项 | Go | JS/TS |
|---|---|---|
| 声明 | func name(x int) int { |
function name(x: number): number { |
| 多返回值 | ✅ 原生支持 func fn() (int, error) |
❌ 用数组/对象包装 |
| 命名返回值 | ✅ func fn() (sum int) |
❌ 无 |
| 不定参数 | func fn(args ...string) |
function fn(...args: string[]) |
| 参数默认值 | ❌ 不支持 | ✅ function fn(x = 1) |
| 函数重载 | ❌ 不支持 | ✅ TS 支持 |
递归函数
go
// 实现数字阶乘
func factorial(n int) (result int) {
if n > 0 {
result = n * factorial(n-1)
fmt.Println("当前的数值是:", n)
fmt.Println("当前的计算结果是:", result)
return
}
return 1
}
func main() {
i := 5
res := factorial(i)
fmt.Printf("%d的阶乘结果是:%d", i, res)
// 输出: 5的阶乘结果是:120
}
Go 的递归和 JS 递归原理一致,都是函数调用栈的"先进后出"。语法上只有声明方式的差异。
11. 字符串处理
字符串定义
go
package main
import (
"fmt"
"strings"
)
func main() {
// 双引号:会解析转义字符 \t \n
s := "\t\txxx\n"
fmt.Println("双引号字符串:", s) // 输出带 tab 和换行
// 反引号(backtick):原样输出,不解析转义
s2 := `\t\txxx\n`
fmt.Println("反引号字符串:", s2) // 原样: \t\txxx\n
// 反引号支持多行
s3 := `
我是杜宽
我主要教授的课程是: 云原生系列,k8s,go,python
`
fmt.Println("多行字符串:", s3)
}
ts
// TS 等效
let s = "\t\txxx\n" // 双引号:解析转义
let s2 = String.raw`\t\txxx\n` // 原始字符串(不解析转义)
let s3 = `
多行
字符串
`
对比 :Go 反引号 = JS 模板字符串的 String.raw 效果;Go 双引号 = JS 普通引号。
字符串常用操作
go
import "strings"
s4 := "dukuan"
s5 := "杜宽"
// 长度(注意:中文一个字符占 3 字节,len 返回字节数)
len(s4) // 6(英文字母,1字节=1字符)
len(s5) // 6(一个中文=3字节,2个中文=6字节!)
// 字符串截取(按字节截取,小心中文乱码)
s4[:2] // "du"
// 大小写
strings.ToUpper(s7) // "DUKUAN"
strings.ToLower(s8) // "dukuan"
strings.Title(s7) // "Dukuan"(Go 1.18+ 弃用)
// 是否包含
strings.Contains(s7, "uk") // true
strings.ContainsAny(s7, "uw") // true(包含任意一个字符)
strings.HasPrefix(s10, "我") // true
strings.HasSuffix(s10, "爱") // false
// 忽略大小写比较
strings.EqualFold(s7, s8) // "dukuan" vs "dUKUan" → true
// 统计子串出现次数
strings.Count("dukuan and dot is me", "u") // 3
// 拆分
strings.Split("a,b,c", ",") // ["a" "b" "c"]
strings.SplitAfter("a,b,c", ",") // ["a," "b," "c"]
// 拼接
strings.Join([]string{"dot", "balo", "du", "kuan"}, " ") // "dot balo du kuan"
// 重复
strings.Repeat("我", 5) // "我我我我我"
// 替换
strings.ReplaceAll("dsad3afd3rq", "3", "dukuan") // 全部替换
strings.Replace("dsad3afd3rq", "3", "杜宽", 1) // 只替换前1个
// 去除首尾指定字符
strings.Trim(" dukuan ,", ",") // " dukuan "
strings.TrimSpace(" dukuan ") // "dukuan"
Go 字符串易错点:
len()返回字节数,不是字符数。中文 UTF-8 占 3 字节,用utf8.RuneCountInString()获取真实字符数- 直接索引
s[0]返回的是字节(byte),不是字符 - 字符串截取
s[:n]按字节切,截中文时可能截出乱码
12. 类型转换
strconv --- 字符串与数值互转
go
package main
import (
"fmt"
"reflect"
"strconv"
)
func main() {
// 数值 → 字符串:Itoa(Integer to ASCII)
i := 7275
sn := strconv.Itoa(i)
fmt.Println(sn) // "7275"
fmt.Println("类型:", reflect.TypeOf(sn)) // string
// 字符串 → 数值:Atoi(ASCII to Integer)
s1 := "50"
i1, _ := strconv.Atoi(s1) // _ 忽略错误返回值
fmt.Println("转换后的数值:", i1) // 50
// 带错误处理的安全转换
s2 := "200abc" // 非数字字符串
i2, err := strconv.Atoi(s2)
if err != nil {
fmt.Println("转换失败,当前值不能转换为数值:", s2)
} else {
fmt.Println("转换成功:", i2)
}
}
ts
// TS 等效
const i = 7275
const sn = String(i) // "7275"
// 或 sn = i.toString()
const s1 = "50"
const i1 = parseInt(s1, 10) // 50
// 或用 Number(s1)
const s2 = "200abc"
const i2 = parseInt(s2, 10) // 200(JS 宽松解析,部分成功)
const n2 = Number(s2) // NaN(严格转换失败)
Go 比 JS 严格得多 :strconv.Atoi("200abc") 返回错误,不会像 JS 那样尝试部分解析。
Parse 系列 --- 字符串解析为各类型
go
// ParseBool:字符串转布尔
b1, _ := strconv.ParseBool("1") // true
b2, _ := strconv.ParseBool("true") // true
b3, _ := strconv.ParseBool("0") // false
// 支持的字符串:1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False
// ParseInt:字符串转整数
// ParseInt(s, 进制, 位数)
n, _ := strconv.ParseInt("80", 10, 8) // 80(十进制解析,存到 int8)
n2, _ := strconv.ParseInt("ff", 16, 16) // 255(十六进制解析,存到 int16)
// ParseFloat:字符串转浮点
f, _ := strconv.ParseFloat("3.14159", 64) // 3.14159(float64)
ts
// TS 等效
Boolean("1") // true
Boolean("0") // true(注意!非空字符串全为 true)
Boolean("") // false
parseInt("80", 10) // 80
parseInt("ff", 16) // 255
parseFloat("3.14159") // 3.14159
数值类型间的强制转换
go
// Go 中不同数字类型需要显式转换
var r int = 5
s := math.Pi * float64(r) * float64(r) // int → float64
// JS 中自动转换,不需要关心
// const r = 5
// const s = Math.PI * r * r // 自动处理
13. 错误处理
Go 没有 try-catch,错误是返回值的一部分。这是前端开发者最需要适应的思维方式转变。
errors.New --- 创建错误
go
package main
import (
"errors"
"fmt"
)
func main() {
// 方式一:errors.New
err := errors.New("这是一个自定义错误")
fmt.Println(err)
// 方式二:fmt.Errorf(支持格式化,类似 fmt.Sprintf)
err2 := fmt.Errorf("这是一个自定义错误: %s, 它是使用fmt生成的", "这是错误内容")
fmt.Println(err2.Error())
}
ts
// TS 等效
const err = new Error("这是一个自定义错误")
const err2 = new Error(`这是一个自定义错误: "这是错误内容", 它是使用fmt生成的`)
函数返回错误模式
go
// Go 函数通常返回 (结果, error)
func division(i1, i2 float64) (res float64, err error) {
fmt.Println("需要计算的数字是:", i1, i2)
if i2 == 0 {
return 0, errors.New("输入的分母不能为0") // 返回错误
}
res = i1 / i2
return res, nil // nil 表示没有错误
}
func main() {
res, err := division(2, 0)
if err != nil {
fmt.Println("计算错误:", err.Error())
return
}
fmt.Println("计算结果:", res)
}
ts
// TS 等效(throw/catch 模式)
function division(i1: number, i2: number): number {
if (i2 === 0) {
throw new Error("输入的分母不能为0")
}
return i1 / i2
}
// 或使用 Result 类型(更接近 Go 风格)
function divisionSafe(i1: number, i2: number): { res: number } | { err: Error } {
if (i2 === 0) {
return { err: new Error("输入的分母不能为0") }
}
return { res: i1 / i2 }
}
| 对比项 | Go | JS/TS |
|---|---|---|
| 错误机制 | 错误作为返回值 | throw / catch 异常抛出 |
| 检查错误 | if err != nil { |
try { } catch(e) { } |
| 无错误 | 返回 nil |
不抛异常 / catch 不触发 |
| 强制处理 | 忽略未用变量编译报错 | 可以不 catch |
Go 错误处理核心模式:
go
result, err := someFunction()
if err != nil {
// 处理错误
fmt.Println("出错了:", err.Error())
return
}
// 正常使用 result
前端开发者适应要点
- Go 中用
if err != nil代替 JS 中的try-catch - Go 函数通常返回
(T, error)二元组,第二个值专门传错误 nil是空错误(没有错误),类似 JS 中不抛出异常时的正常状态- Go 没有异常堆栈的概念(除非用
panic)
14. defer / panic / recover
这三个关键字是 Go 独特的错误处理和控制流机制,没有直接的 JS 对应物。
panic --- 程序异常终止
go
package main
import (
"errors"
"fmt"
)
// 模拟数据库连接
func connectDatabase(address string, port int) (string, error) {
if address == "" || port == 0 {
return "", errors.New("无法链接数据库")
}
return "数据库链接成功", nil
}
func main() {
s, err := connectDatabase("", 0)
if err != nil {
fmt.Println(err)
panic(err) // 程序直接退出,后续代码不再执行
}
fmt.Println(s)
// 切记:panic 不能滥用,只有程序无法继续正常工作时才用
}
JS 类比 :panic ≈ throw new Error() + 不打算被捕获(程序崩溃)。但 Go 中 panic 会导致整个程序退出,比 JS 的未捕获异常更"重"。
defer --- 延迟执行
defer 用于在函数返回前执行的清理操作(关闭文件、释放锁、记录日志等)。
go
package main
import "fmt"
func returnDataToFrontend(msg string) {
fmt.Println("返回给前端的数据是:", msg)
}
func main() {
msg := "返回给前端的数据"
defer returnDataToFrontend("1") // 最后执行
defer returnDataToFrontend("2") // 倒数第二
defer returnDataToFrontend("3") // 倒数第三
defer returnDataToFrontend("4") // 倒数第四
defer returnDataToFrontend(msg) // 倒数第五(最先执行的 defer)
// panic 之前 defer 也会执行
_, err := connectDatabase("", 0)
if err != nil {
fmt.Println(err)
panic(err)
}
}
// 输出顺序(先进后出):
// 返回给前端的数据是: 返回给前端的数据 (msg 的值在 defer 注册时已确定)
// 返回给前端的数据是: 4
// 返回给前端的数据是: 3
// 返回给前端的数据是: 2
// 返回给前端的数据是: 1
defer 核心特性:
- 先进后出(栈结构):多个 defer 注册顺序和执行顺序相反
- 参数快照:defer 注册时参数值就已确定,后续变量修改不影响
- 在 return 之后、函数真正返回之前执行
- panic 之前也会执行:所以 defer 可以用于异常恢复
JS 类比:
ts
// JS 中类似 defer 的模式
function withDefer() {
try {
// do something...
return result
} finally {
cleanup() // ≈ defer
}
}
// 但 Go 的 defer 更灵活,可以在函数任意位置注册
// 多个 defer 的执行顺序(先进后出)= JS Promise 微任务队列
recover --- 异常捕获与恢复
recover 只能在 defer 函数中使用,用来捕获 panic 并阻止程序崩溃。
go
package main
import "fmt"
func printSliceData(s []string) {
// recover 必须放在 defer 中
defer func() { // 匿名函数
fmt.Println("程序执行失败, 捕获异常")
if err := recover(); err != nil {
// recover 返回 panic 传入的值
fmt.Println("捕获到了一个错误:", err)
// 可以做:发告警、记日志、返回友好提示给前端
}
}()
fmt.Println("切片的内容:", s)
fmt.Println("切片的第三个值是:", s[2]) // 如果 s 长度不够会 panic
}
func main() {
s := []string{"a", "b"} // 只有2个元素
printSliceData(s) // 访问 s[2] 会 panic,但被 recover 捕获
fmt.Println("程序继续执行") // 这行会执行!
}
defer + recover 完整模式 ≈ JS 中的:
ts
// JS 类比
try {
console.log(s[2]) // undefined,不会报错
} catch (e) {
console.log("捕获到了一个错误:", e)
}
// 程序继续执行
重要区别:
| 概念 | Go | JS/TS 类比 |
|---|---|---|
defer |
延迟执行,函数退出前执行 | finally 块(但不完全一样) |
panic |
程序异常终止 | throw(未捕获的异常) |
recover |
在 defer 中捕获 panic | catch 块 |
| 组合模式 | defer func() { if r := recover(); r != nil { } }() |
try { } catch(e) { } finally { } |
Go 独有规则:
recover只有在defer函数内部直接调用时才有效- 处理完 panic 后程序继续正常运行,不会退出
- 一般只用于:高可用服务不希望单个请求异常导致整个进程退出
附录:与 JS/TS 语法速查表(基础)
| 场景 | Go | JS/TS |
|---|---|---|
| 变量声明 | var x int = 1 / x := 1 |
let x: number = 1 |
| 常量 | const x = 1 |
const x = 1 |
| 函数 | func fn(x int) int { |
function fn(x: number): number { |
| 箭头函数 | ❌ 没有,用匿名函数 | const fn = () => {} |
| 多返回值 | func fn() (int, error) |
function fn(): [number, Error] |
| if | if x > 1 { |
if (x > 1) { |
| for | for i := 0; i < 10; i++ { |
for (let i = 0; i < 10; i++) { |
| while | for x > 0 { |
while (x > 0) { |
| switch | switch x { case 1: ... } |
switch (x) { case 1: ... } |
| 数组 | [3]int{1,2,3} |
[1, 2, 3] |
| 切片/动态数组 | []int{1,2,3} |
[1, 2, 3] |
| map | map[string]int{"a": 1} |
{ a: 1 } 或 new Map() |
| 指针 | var p *int = &x |
❌ 无直接对应 |
| 错误 | if err != nil { |
try { } catch(e) { } |
| 延迟执行 | defer fn() |
finally { } 块 |
| 展开 | fn(slice...) |
fn(...array) |
| 空值 | nil |
null / undefined |
| 类型检查 | reflect.TypeOf(x) |
typeof x |
| 模版字符串 | fmt.Sprintf("x=%d", x) |
x=${x} |
| 字符串拼接 | "a" + "b" 或 strings.Join |
"a" + "b" 或模板字符串 |