尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

GO 的使用

GO 的使用 目录一、GO 语言规范Comments注释Tokens记号Semicolons分号二、GO 语言基础详解1. 变量、常量、类型、零值、:2. 控制流if / for / switch / defer一、GO 语言规范原文参考GO 语言规范Comments注释注释用作程序文档。有两种形式行注释以//开始到行末结束。一般注释以/*开始到其后第一个*/结束。注释不能开始于rune/字符串字面量内部或另一个注释内部。不含换行的一般注释 → 行为像空格其它注释 → 行为像换行。Tokens记号记号构成 Go 的“词汇表”分四类标识符identifiers关键字keywords运算符与标点operators and punctuation字面量literals空白空格 U0020、水平制表 U0009、回车 U000D、换行 U000A通常被忽略除非用来把本会粘成一个记号的字符分开。另外换行或文件结束可能触发插入分号。切分记号时取能构成合法记号的最长字符序列最长匹配。Semicolons分号形式语法里多处用;作终结符。Go 程序可按下面两条规则省略大部分分号规则 1 — 自动插入把输入切成记号后若某行的最后一个记号是下面之一则在该记号后立即自动插入分号标识符整数 / 浮点 / 虚数 / rune / 字符串字面量关键字break、continue、fallthrough、return运算符与标点、--、)、]、}规则 2 — 单行复合写法为允许复杂语句写在同一行闭括号)或}之前的分号可以省略。规范中的示例按惯用法省略分号。二、GO 语言基础详解1. 变量、常量、类型、零值、:要点Go 是静态类型语言变量类型在编译期确定。零值变量声明后未赋值时的默认值很重要。:只能在函数内使用用于短变量声明并自动推断类型。var可在包级或函数内使用。package main import fmt // 包级变量必须用 var不能用 : var appName demo // 常量编译期确定不能再改 const MaxRetry 3 // iota常量计数器常用于枚举 const ( StatusPending iota // 0 StatusRunning // 1 StatusDone // 2 ) func main() { // ---------- var 声明 ---------- var a int // 零值0 var b string // 零值 var c bool // 零值false var d *int // 零值nil fmt.Println(a, b, c, d) // 声明并赋值 var x int 10 var y 20 // 类型推断为 int // 多变量声明 var m, n int 1, 2 // ---------- : 短声明仅函数内---------- name : Alice // 等价于 var name string Alice age : 18 ok : true // 类型转换必须显式转换没有隐式转换 var i int 42 var f float64 float64(i) // 一次打印方便观察 fmt.Println(x, y, m, n, name, age, ok, f, appName, MaxRetry, StatusDone) }常见零值表类型零值int,float64等数值0boolfalsestring指针、切片、map、channel、接口、函数nil结构体每个字段都是其类型的零值2. 控制流if/for/switch/deferpackage main import ( fmt os ) func main() { // ---------- if ---------- score : 85 if score 60 { fmt.Println(及格) } else if score 80 { // 注意Go 的 else if 写法 fmt.Println(良好) } else { fmt.Println(不及格) } // if 支持短语句先执行赋值再判断作用域只在 if/else 内 if err : doSomething(); err ! nil { fmt.Println(出错:, err) return } // ---------- forGo 只有 for没有 while---------- // 经典三段式 for i : 0; i 3; i { fmt.Println(i , i) } // while 风格 n : 3 for n 0 { fmt.Println(n , n) n-- } // 无限循环 // for { // break // } // range 遍历 nums : []int{10, 20, 30} for idx, val : range nums { fmt.Println(idx, val) } // 只要下标for idx : range nums // 只要值 for _, val : range nums // ---------- switch ---------- day : 2 switch day { case 1: fmt.Println(周一) case 2, 3: // 多个值共用一个分支 fmt.Println(周二或周三) // // 匹配到 case 后自动结束不会掉进下一个 case default: fmt.Println(其他) } // Go 的 case 默认 break不会贯穿需要贯穿用 fallthrough // switch 后面可以跟变量和短语句 var n int 10 switch x : n % 2; x { // 先算 x再匹配 case 0: fmt.Println(偶数) default: fmt.Println(奇数) } // switch 可不写条件当 if-else 链用 age : 20 switch { case age 18: fmt.Println(未成年) case age 60: fmt.Println(成年) default: fmt.Println(老年) } // fallthrough 很少用 switch n : 1; n { case 1: fmt.Println(one) fallthrough // 强制进入下一个 case即使条件不匹配 case 2: fmt.Println(two) // 也会执行 } // ---------- defer ---------- // defer函数返回前按 LIFO后进先出执行 // 常用于关闭资源保证一定会执行 f, err : os.Open(not-exist.txt) if err ! nil { fmt.Println(打开失败演示 defer 仍可学:, err) } else { defer f.Close() // 即使后面出错也会在 return 前关闭 fmt.Println(文件打开成功) } defer fmt.Println(第 1 个 defer) // 后执行 defer fmt.Println(第 2 个 defer) // 先执行 fmt.Println(函数主体) } func doSomething() error { return nil }defer要点参数在 defer 注册时 求值。多个 defer后注册的先执行。适合Close()、Unlock()、回滚事务等。1类型switch配合接口/anyfunc printType(v any) { switch t : v.(type) { // t 是断言后的具体类型值 case int: fmt.Println(int:, t) case string: fmt.Println(string:, t) case nil: fmt.Println(nil) default: fmt.Printf(other: %T %v\n, t, t) } }工作中常见写法HTTP 错误码映射func httpStatus(err error) int { switch { case err nil: return 200 case errors.Is(err, ErrNotFound): return 404 case errors.Is(err, ErrBadRequest): return 400 default: return 500 } }写法含义switch x按x的值匹配switch按各case条件true/false匹配switch a : ...; x先执行短语句再按x匹配switch v : x.(type)按接口的动态类型匹配不能跟的典型例子不能写成switch后面直接一长串复杂逻辑却不构成合法表达式case的类型必须和 switch 表达式类型兼容类型 switch 除外。注意.(type)只能写在switch里不能单独写在普通语句中。2channel是 goroutine 之间传递数据的管道Channel 的作用在协程之间安全地传递数据并顺便完成同步。Go 的理念是“不要通过共享内存来通信而要通过通信来共享内存。” Channel 就是这个通信管道。具体解决什么问题没有 channel 时多个协程要共享变量通常要加锁容易写错。有了 channel传数据 — 一个协程发另一个协程收同步 — 发/收会阻塞天然协调“谁先谁后”避免直接抢同一块内存 — 数据经 channel 交接所有权更清晰代码里就是典型用法子协程算好结果通过 channel 交给maingo func() { ch - 42 // 发送 }() v : -ch //接收这里 channel 既把42传过去又让main等到结果就绪才继续。两种常见用法类型作用侧重无缓冲make(chan int)强同步发和收必须同时就绪像“当面交接”有缓冲make(chan T, n)异步队列先放进缓冲区满了才阻塞Channel 协程之间的管道用来传值、排队、通知完成让并发协作时不必自己到处加锁。① 创建与收发package channel import fmt // 函数名首字母小写表示是私有函数只能在这个包内部使用 // 函数名首字母大写表示公有函数可以调到别的地方使用 func Chann() { ch : make(chan int) // 无缓冲 channel发送方和接收方必须同时准备好否则阻塞 go func() { ch - 42 // 发送 }() v : -ch //接收 fmt.Println(v) }② 有缓冲channelfunc Chann2(){ // 缓冲容量为 2没满之前发送不阻塞 ch : make(chan string,2) ch - a ch - b // ch - c // 再发会阻塞直到有人接收 fmt.Println(-ch) // a fmt.Println(-ch) // b }类型写法特点无缓冲make(chan T)同步发与收必须配对有缓冲make(chan T, n)异步缓冲未满可先发单个 channel 上发送和接收都按 FIFO先进先出顺序进行。发送同一时刻只能有一个 goroutine 真正完成一次发送内部有锁。多个 goroutine 同时ch - x时谁先拿到锁谁先发发成功的顺序就是进入队列的顺序。无缓冲必须等有人接收一对一同步完成。有缓冲缓冲区未满时发送立刻入队满了就阻塞直到有人取走再发。接收同样 FIFO先进入 channel 的值先被-ch取走。无缓冲发送和接收必须“同时就绪”一次交接一个值。有缓冲从缓冲区头部取空了就阻塞等有新数据。close之后还能按顺序把剩余值读完读完后再收得到零值且ok false。for range ch按顺序读到 channel 关闭且数据取完为止。③ 关闭 channel 与 rangefunc Chann3() { ch : make(chan int, 3) ch - 1 ch - 2 ch - 3 close(ch) // 关闭后不能再发送还能继续接收剩余数据 // range 会读到关闭为止 for v : range ch { fmt.Println(v) } // 也可手动判断是否关闭 // v,ok : -ch // okfalse 表示已关闭且读完 }规则只由发送方关闭 channel接收方关容易 panic向已关闭 channel 发送会 panic从已关闭 channel 接收先读完剩余值再得到零值ok false注意场景行为单 channel、单发送方发送顺序 接收顺序多发送方写同一 channel各次发送仍按完成顺序排队但“谁先发完”不由你写代码顺序保证多个 channel彼此独立没有统一顺序select多路就绪时随机选一个同一个 channel 保证“先发的先收”跨多个发送方或多个 channel只保证每个 channel 内部有序不保证全局时序。④ 简单补充内容一个函数不是一个进程go启动的也不是进程而是协程goroutine。三者关系概念是什么例子进程 (Process)操作系统里的一个程序实例有独立内存运行的整个 Go 程序线程 (Thread)进程里的执行单元OS 调度Go runtime 内部用的 OS 线程协程 (Goroutine)Go 自己调度的轻量执行流go func() { ... }()一个 Go 程序通常是 1 个进程里面有若干 OS 线程再上面跑很多 goroutine。进程整个程序└── OS 线程runtime 管理默认约等于 CPU 核数└── 大量 goroutine由 Go 调度器切换所以func Chann1()→ 只是一段代码被调用时在某个 goroutine 里跑go func() { ch - 42 }()→ 新建一个 goroutine不是新进程整个main程序 → 才是一个 进程协程的执行顺序没有固定、可依赖的先后顺序。Go 调度器会在多个 goroutine 之间切换谁先跑、谁后跑取决于CPU 核数、GOMAXPROCS谁先就绪、谁在阻塞如等 channel调度器何时抢占/切换代码里的这段go func() { ch - 42 // 发送 }() v : -ch //接收实际可能是main先跑到-ch阻塞 → 调度器跑子协程发42→main被唤醒收到或子协程先发完main再收两种都合法。能保证的是同步点channel / WaitGroup 等不能保证“谁先执行几行代码”。和“函数”的关系普通函数调用在当前协程里顺序执行完再返回go f()在新协程里跑f当前协程立刻继续往下走一个协程里可以调用很多函数一个函数也可以被很多协程同时调用程序启动 一个进程main是第一个协程go xxx再开更多协程调度器决定它们交错执行顺序不可靠要用 channel / WaitGroup 等显式同步。含义何时发生并发多个协程交替推进几乎总是单核也能并发并行同一时刻真的在多个 CPU 上跑多核 多个可运行的协程时Go 调度器默认会用多个 OS 线程约等于 CPU 核数。所以多个协程可能同时跑在不同核上也可能只是交替跑。写代码时不要假设“一定并行”只假设“可能同时推进”。什么时候需要协程 有事可以“同时做”不想互相干等时才用协程。适合用I/O 等待 — 网络请求、读文件、查数据库。等的时候 CPU 闲着开协程可以同时干别的。彼此独立的任务 — 比如同时请求 10 个 API再汇总结果。后台持续干活 — 定时任务、消息消费、心跳不阻塞主流程。生产者 / 消费者 — 一边生产数据一边处理用 channel 衔接。不适合 / 不必用纯顺序逻辑一步依赖上一步 → 普通函数调用就够。极短的计算开协程的调度开销可能比收益大。只是为了“看起来高级”而乱开 → 反而难调试、难同步。简单对照不用协程A 做完 → B 做完 → C 做完总时间 ≈ ABC用协程 A、B、C 同时推进再用 channel/WaitGroup 汇合总时间 ≈ 最慢那个之前的例子子协程发数据、main收数据就是典型场景——一边干活一边等结果用 channel 对接。协程用来表达“可以同时推进的工作”channel 用来在这些工作之间传数据和同步。没有并发需求时别用协程。子协程完全可能比主协程main更早跑完。调度不保证谁先谁后所以常见两种情况都合法子协程先结束 — 它把活干完就退出main还在继续跑后面的代码。main先结束 — 整个程序立刻退出还没跑完的子协程会被直接干掉不会等它们。例如func main() { go func() { // 有可能很快执行完也有可能还没开始就被程序结束了 fmt.Println(子协程) }() // 如果这里 main 马上 return子协程经常来不及打印 }所以子协程可以比main先结束。但如果希望一定看到子协程的结果不能靠运气要用 channel、sync.WaitGroup等让main等它完成。之前的写法就是在等go func() { ch - 42 }() v : -ch // main 会阻塞到子协程发完保证能收到⑤ 只读 / 只写函数签名里很有用// 只允许发送 --- 生产者 func producer(out chan- int){ for i : 0 ; i 3 ; i{ out - i } close(out) // 关闭通道 } // 只允许接收 --- 消费者 func consumer(in -chan int){ for v : range in{ fmt.Println(got,v) } } func Chann4() { ch : make(chan int) go producer(ch) // 用协程来执行 consumer(ch) }⑥select同时等待多个 channelfunc SelectDemo() { ch1 : make(chan string) ch2 : make(chan string) go func() { ch1 - from ch1 }() go func() { ch2 - from ch2 }() for i : 0; i 2; i { select { // 若当时两个 channel 都还没数据 → 当前协程在 select 上阻塞 // 阻塞后调度器会去跑其他就绪的协程那两个发送协程如果就绪就可能被跑到 //某个 channel 被写入后 → select 被唤醒进入对应 case case msg : -ch1: fmt.Println(msg) // 如果ch1 先获得数据那么就执行这个case case msg : -ch2: fmt.Println(msg) // 如果ch2 先获得数据那么就执行这个case } // 如果两个channel都有数据那么就随机选择一个不是按完成先后选择 } // select 看的是此刻哪些 case 就绪只就绪一个就选它多个就绪就随机选一个都不就绪就阻塞直到有人往 channel 里写。 }带超时API 里很常见func WithTimeout() { ch : make(chan int) go func() { // time.Sleep(2 * time.Second) // 加上这句就可以看到输出超时信息 ch - 42 }() select { case v : -ch: // 从通道中接收数据 fmt.Println(收到, v) case -time.After(time.Second * 2): // time.After(2 * time.Second) 会返回一个 channel // 约 2 秒后这个 channel 会收到一个时间值 fmt.Println(超时) } }超时部分等价于timeout : time.After(2 * time.Second) // 拿到管道 case t : -timeout: // 从管道取出时间赋给 t _ t // 但超时场景里通常用不到 t超时场景里要的只是“这个 channel 变得可读了”这个信号不需要时间本身所以写成case -time.After(2 * time.Second): // 接收但丢弃值管道也没有起名字非阻塞尝试func FeiZuSe() { ch : make(chan int) select { case ch - 1: // 没人接就一直阻塞 -- 这是发送方被阻塞了 fmt.Println(发送成功) default: fmt.Println(通道忙跳过) } // 这段里没有任何接收方 → ch - 1 不能立刻完成于是走 default打印 通道忙跳过 // 接收方被阻塞的情况下 //select { //case v : -ch: // 没有接收的数据 // fmt.Println(收到, v) //default: // fmt.Println(没有数据不等) } }加上selectdefault后规则变成先检查各个case能不能立刻完成能 → 执行那个case这里就是发送都不能 → 立刻走default不阻塞所以这里没有“等”只有“试一下不行就跳过”。⑦ 工作中典型用法并发查、汇总结果func FetchAll() { ids : []int{1, 2, 3, 4, 5} ch : make(chan string, len(ids)) for _, id : range ids { id : id // 避免闭包踩坑 go func() { // 模拟 RPC/DB ch - fmt.Sprintf(user-%d, id) }() } out : make([]string, 0, len(ids)) for range ids { out append(out, -ch) } fmt.Println(out) }常见坑死锁无缓冲 channel 在同一个 goroutine 里又发又收且没有别人配合泄漏goroutine 卡在ch -/-ch永远等不到重复 close 会 panic请求处理里乱开 goroutine channel忘记用context取消后续内容请看下一章节
返回列表