Go 语言基础笔记 — 面向 JS/TS 前端开发者

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 中变量声明后一定有值(零值),不会出现 undefined
    • string 默认 ""
    • int 默认 0
    • bool 默认 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 是强静态类型语言。intint64 是不同的类型,必须显式转换才能一起运算。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 条件中当表达式用(但 forpost 语句中可以用)。


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 一种循环关键字(没有 whiledo-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 易错点

  1. var m map[K]V 声明得到的是 nil,不能直接赋值,必须用 make 初始化
  2. 访问不存在的 key 返回零值,不报错
  3. val, ok := m[key] 的 "comma ok" 模式判断是否存在
  4. 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 类比panicthrow 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 核心特性

  1. 先进后出(栈结构):多个 defer 注册顺序和执行顺序相反
  2. 参数快照:defer 注册时参数值就已确定,后续变量修改不影响
  3. 在 return 之后、函数真正返回之前执行
  4. 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" 或模板字符串
相关推荐
鹏北海4 小时前
Go 语言进阶笔记 — 面向 JS/TS 前端开发者
go
鹏北海4 小时前
Go 包管理笔记 — 面向 JS/TS 前端开发者
go
百度Geek说5 小时前
告别死锁和陈旧语法、告别性能瓶颈:新手Gopher 秒变 Go 语言大神
人工智能·go
用户3983461612012 小时前
Go-Spring 实战第 14 课 —— Bean 注册函数:Provide、Module、Group 以及 Configuration
spring·go
锋行天下1 天前
一句mysql复杂查询搞崩一个壮汉
后端·mysql·go
用户398346161201 天前
Go-Spring 实战第 13 课 —— Bean 元信息:名称、生命周期、接口导出、条件和显式依赖
spring·go
猪猪拆迁队1 天前
用 ESP32-S3 和 TinyGo,先搭个 AI 语音助手的小底座
前端·后端·go
赫媒派2 天前
炸裂!Go 1.26 三连发:go fix 现代化、pkg.go.dev API 开放、源码级内联器
go
用户398346161202 天前
Go-Spring 实战第 11 课 —— 依赖注入的目标:单 Bean 注入和集合注入
spring·go