前言
go语言自2012年正式版(v1.0)发布以来就备受关注,特别是docker和k8s的问世,让go语言更火了一把,如今潮水退去,是否还更值得我们学习? 答案是肯定的! go语言有如下特点:
- 简洁的语法(你写过js等语言就能看懂代码)
- 静态类型和编译型(编译成二进制文件产物体积非常小)
- 垃圾回收:自动内存管理,无需手动分配和释放内存。
- 原生支持并发(
goroutine
) - 生态强大:发布时间较长,通过了时间的检验,有丰富的生态库、疑难杂症都有解决方式(很多坑前人都已经踩过了),更适合生产了。
- 口碑不错,很多公司都有go语言的后端项目。
综上所述,go算是在纯静态语言和动态语言中取了平衡。 在不追求极致性能的情况下,也是一个不错的选择。
(ps1:追求极致性能推荐使用Rust,关于Rust可以参考一下我之前的这篇博客:juejin.cn/post/738991...
(ps2:如果对性能要求不高,且团队内部JSer较多,nodejs+nestjs也是不错的选择,此文后续我也会书写😄)
因为go语法和js很像,而且本人对ts/js比较熟悉,本文会使用js语言做类比,学起来会很快的😄。
本文分为三大块:golang预发快速学、goframe接口开发、langchain ai开发。
标题解释
本人比较喜欢《凡人修仙传》,借用该小说的等级概念做一下类比,凡人就是啥也不会的小白,筑基期,相当于是已经入门,可以独当一面了。看完本文相信你的golang水平可以达到"筑基期"了。
环境准备
基础安装(必须)
go安装
mac 用户推荐使用brew 安装
bash
brew install go
锦上添花(可先跳过)
idea
idea 推荐使用goland,开发语言我一般都推荐"拎包入住"的方式,很多插件和配置直接内置了,特别适合新手。
mysql
类似于mysql这种基础服务,推荐使用docker方式,推荐理由还是"拎包入住"。
ollama
类似于ai模型的"docker",模型推荐使用llama3.1即可,个人电脑也能跑起来,如果跑不起来的,可在ollama 官网查看模型运行要求。
语法速学
hello world
创建文件
bash
touch main.go
编写代码
go
package main
import "fmt"
func main() {
fmt.Println("main func ")
}
运行
go
go run main.go
控制台打印:

如上hello world已经打印出来,至此我们已经入门90%了😆
代码讲解
package main
- 每个文件必须含有一个package名,我们这个文件的名字为main
- 运行go run 的时候必须是package为main的文件,main和我们nodejs 的package.json文件 main(入口文件)字段有异曲同工之妙(其实主线程)
注意点:
- 和js不同的点,多个文件可以一个package name
- package之间引用无需export操作,只要变量名/函数名是大写字母开头,其他文件即可访问/调用。
go
import (
"fmt"
"go-study/test"
)
func main() {
fmt.Println("main func ", test.Name)
}
init函数
每个文件都可以包含 init 函数,该package被引用时,init函数都会执行(无需显式调用)。
go
package test
import "fmt"
func init() {
fmt.Printf("test2 init\n")
}
示例代码:
main.go
go
package main
import (
"fmt"
"go-study/test"
)
func main() {
fmt.Println("main func ", test.Name)
}
func init() {
fmt.Println("init func ")
}
test/pkg1.go
go
package test
import "fmt"
var Name = "李云龙"
var age = 40
func init() {
fmt.Printf("test init\n")
}
test/pkg2.go
go
package test
import "fmt"
func init() {
fmt.Printf("test2 init\n")
}
fmt.Println
和js console.log类似可以通过逗号分隔打印多个值:
go
fmt.Printf("main func", test.Name)
fmt.Printf
可以使用占位符打印:
go
package main
import "fmt"
func main() {
name := "张三"
age := 25
price := 19.99
isActive := true
// 字符串
fmt.Printf("姓名: %s\n", name) // 姓名: 张三
// 整数
fmt.Printf("年龄: %d\n", age) // 年龄: 25
// 浮点数
fmt.Printf("价格: %.2f\n", price) // 价格: 19.99
// 布尔值
fmt.Printf("激活状态: %t\n", isActive) // 激活状态: true
// 类型
fmt.Printf("类型: %T\n", name) // 类型: string
}
类似于js的模版字符串,不过可以控制的更精细,是要打印值本身还是还是打印类型等。js则是默认打印的是变量的值。
变量声明
var 关键字声明
变量声明方式如下:

果你没有显式为变量赋予初值,Go 编译器会为变量赋予这个类型的零值(这个类型的默认值)
csharp
var a int // a的初值为int类型的零值:0
各种类型零值如下

变量声明块
如下可以批量声明变量
go
var (
a int = 128
b int8 = 6
s string = "hello"
c rune = 'A'
t bool = true
)
var a1, b1, c1 int = 5, 6, 7
简洁写法
省略变量类型:
go
var a, b = 5, 6
类似于ts的类型推断
省略关键字var
go
a := 12
b := 'A'
c := "hello"
这是go的语法糖
变量作用域:
作用域类似于js的let const
的块级作用域,{}
内可重新声明变量,内层可访问外层变量,且返回的是最里层的变量。 例子如下:
go
package main
var a = 11
func main() {
println("a11", a)
{
var a = 20
println("a21", a)
{
var a = 30
println("a31", a)
{
println("a4", a)
}
println("a32", a)
}
println("a22", a)
}
println("a12", a)
}
数字类型
整型
如下为整型类型,注意不要超过该类型的范围 例如:int8范围为:-128 到 127(ps:2的7次方有1位为符号位)
超过范围则有整型溢出的问题
go
var s int8 = 127
s += 1 // 预期128,实际结果-128
var u uint8 = 1
u -= 2 // 预期-1,实际结果255
go
package main
import (
"fmt"
"unsafe"
)
func main() {
// 有符号整数示例
var a int8 = 127
var b int16 = 32767
var c int32 = 2147483647
var d int64 = 9223372036854775807
var e int = 100 // 平台相关
// 无符号整数示例
var f uint8 = 255
var g uint16 = 65535
var h uint32 = 4294967295
var i uint64 = 18446744073709551615
var j uint = 200 // 平台相关
// byte 和 rune
var k byte = 65 // uint8 的别名,ASCII 'A'
var l rune = '中' // int32 的别名,Unicode 码点
fmt.Println(a, b, c, d, e, f, g, h, i, j, k, l)
fmt.Printf("int8: %d, 大小: %d 字节\n", a, unsafe.Sizeof(a))
fmt.Printf("int16: %d, 大小: %d 字节\n", b, unsafe.Sizeof(b))
fmt.Printf("int32: %d, 大小: %d 字节\n", c, unsafe.Sizeof(c))
fmt.Printf("int64: %d, 大小: %d 字节\n", d, unsafe.Sizeof(d))
fmt.Printf("int: %d, 大小: %d 字节\n", e, unsafe.Sizeof(e))
fmt.Printf("byte: %c, 数值: %d\n", k, k)
fmt.Printf("rune: %c, Unicode: %U\n", l, l)
}
相比js,静态语言就是这样的,内存精细化管理😄 新手可以先不考虑这些可以直接使用 int
或者unit
类型
浮点型
go
package main
import (
"fmt"
"unsafe"
"math"
)
func main() {
// 浮点数声明
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793
// 短变量声明
price := 19.99 // 默认为 float64
// 科学计数法
avogadro := 6.02214076e23 // 阿伏伽德罗常数
electronMass := 9.1093837e-31 // 电子质量
fmt.Printf("float32: %f, 大小: %d 字节\n", f32, unsafe.Sizeof(f32))
fmt.Printf("float64: %.15f, 大小: %d 字节\n", f64, unsafe.Sizeof(f64))
fmt.Printf("price: %f, 类型自动推断为 float64\n", price)
fmt.Printf("阿伏伽德罗常数: %e\n", avogadro)
fmt.Printf("电子质量: %e\n", electronMass)
}
注意 :float默认为float64
还有复数、自定义数据类型等读者可自行了解
字符串
声明
字符串比较简单
go
package main
import (
"fmt"
"unsafe"
)
func main() {
// 多种声明方式
var s1 string = "Hello, 世界"
var s2 = "Go语言"
s3 := "字符串"
// 零值
var zeroString string
fmt.Printf("零值: '%s'\n", zeroString) // 输出空字符串
fmt.Printf("s1: %s\n", s1)
fmt.Printf("s2: %s\n", s2)
fmt.Printf("s3: %s\n", s3)
fmt.Printf("字符串大小: %d 字节\n", unsafe.Sizeof(s1))
}
字符串拼接
go
package main
import (
"fmt"
)
func main() {
var s1 string = fmt.Sprintf("姓名 %v ", "李云龙")
var s2 = s1 + "职业:军人"
fmt.Println(s1, s2)
}
s1
可达到js模板字符串的效果,使用方式和上文介绍的fmt.Printf
一样 s2
类似于js的字符串拼接(字符串加法)。
下标访问具体index
类似于js,可以通过str[0]
的方式访问字符串某位字符 不过go返回的是字符串中特定下标上的字节,而不是字符。
go
package main
import (
"fmt"
)
func main() {
var s = "中国人"
fmt.Printf("0x%x\n", s[0]) // 0xe4:字符"中" utf-8编码的第一个字节
}
如果想达到访问某位字符的效果,可转换类型
go
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
var s = "中国人"
ch, size := utf8.DecodeRuneInString(s)
println(ch, size)
fmt.Printf("%c \n", ch)
}
注意使用format打印,否则打印出来的将是unicode码
字符串遍历
go
package main
import (
"fmt"
)
func main() {
var s = "中国人"
for i := 0; i < len(s); i++ {
fmt.Printf("s[%d] = 0x%x\n", i, s[i])
}
}
可以使用for循环进行字符串遍历,但是效果和字符串下标访问一样,返回的是字节,而不是字符。
可通过for range的方式遍历
go
package main
import "fmt"
func main() {
var s = "Hello,世界!"
fmt.Println(" 使用 for range 循环")
for i, ch := range s {
fmt.Printf("位置 %d: 字符 '%c' (Unicode: U+%04X)\n", i, ch, ch)
}
}
字符串长度
go
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
var s = "Hello,世界! 🌍"
byteCount := len(s) // 字节数
charCount := utf8.RuneCountInString(s) // 字符数
fmt.Printf("字符串: %s\n", s)
fmt.Printf("字节数: %d\n", byteCount)
fmt.Printf("字符数: %d\n", charCount)
}
总之字符串的操作,不符合预期时,优先考虑该操作返回的是"字节"还是"字符"
常量
和js一样,声明常量使用const
go
import "fmt"
const Pi float64 = 3.14159265358979323846 // 单行常量声明
// 以const代码块形式声明常量
const (
size int64 = 4096
i, j, s = 13, 14, "bar" // 单行声明多个常量
)
func main() {
fmt.Println(Pi, i, j, s)
}
不过,Go 常量的类型只局限于Go 基本数据类型,包括数值类型、字符串类型,以及只有两个取值(true 和 false)的布尔类型。
数组
声明方式
无初始值
css
// var 变量名 [长度]类型
var arr [N]T
go
package main
import (
"fmt"
)
func main() {
var arr [10]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
fmt.Println("数组:", arr) // 6
}
有初始值
go
package main
import (
"fmt"
)
func main() {
var arr = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("数组:", arr) // 6
}
不指定数组长度
go
package main
import (
"fmt"
)
func main() {
var arr = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("数组:", arr, len(arr)) // 6
}
指定某位置初始值
go
package main
import (
"fmt"
)
func main() {
var arr = [...]int{9: 99}
fmt.Println("数组:", arr, len(arr))
// 数组: [0 0 0 0 0 0 0 0 0 99] 10
}
如上,数组长度10,第10位为99,其他位置均为int零值 0
遍历
类似于字符串的遍历:
go
package main
import (
"fmt"
)
func main() {
var arr = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("数组:", arr) // 6
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
for _,v := range arr {
fmt.Println(v)
}
}
切片
切片和数组类似,声明仅仅是少了一个"长度"属性,切片比较像js的数组,可不指定长度,且数组长度会随值变化而变化
go
package main
import "fmt"
func main() {
var nums = []int{1, 2, 3, 4, 5, 6}
fmt.Println(len(nums)) // 6
nums = append(nums, 7) // 切片变为[1 2 3 4 5 6 7]
fmt.Println(len(nums)) // 7
}
map
map为go的复合类型,map比较像ts的Record类型,js的object 声明方式如下:
c
// map[key_type]value_type
map[string]string // key与value元素的类型相同
map[int]string // key与value元素的类型不同
可初始化值,也可后续追加,也可后续删除某项。
go
package main
import "fmt"
func main() {
var m = map[string]int{
"a": 1,
"b": 2,
}
m["name"] = 1
fmt.Println(m)
delete(m, "name")
fmt.Println(m)
fmt.Println(len(m))
fmt.Println(m["a"])
}
访问值
go
v, ok := m["key"]
第二个参数可以判断某个key是否在map中,如果不存在v值为map的零值,如下例子不存在的key值为0:
go
package main
func main() {
var m = map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
v, ok := m["cc"]
if ok {
println("含有key", v)
} else {
println("不含有key", v)
}
v2, ok2 := m["c"]
if ok2 {
println("含有key", v2)
} else {
println("不含有key", v2)
}
}
遍历
可以使用for range
遍历
go
package main
import "fmt"
func main() {
var m = map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
for k, v := range m {
fmt.Println(k, v)
}
}
注意:遍历返回的key是无序,请看如下的例子:
go
package main
import "fmt"
func main() {
var m = map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
for k, v := range m {
fmt.Println(k, v)
}
for k, v := range m {
fmt.Println(k, v)
}
for k, v := range m {
fmt.Println(k, v)
}
}
这点和js的对象有点像,不过js的key会先打印数字key,再是字符串key。 go中map的key的打印则是无序。遍历多次顺序会是一样,但是每次运行go run
打印的key顺序会不一样。
自定义类型
类似于ts的自定义type
如下:定义一个新类型T type 类型名 原类型
go
package main
type T1 int
type T2 T1
type T3 string
func main() {
var n1 T1
var n2 T2 = 5
n1 = T1(n2) // ok
var s T3 = "hello"
println(n1, n2, s)
}
结构体 struct
结构体类似于ts的interface
,key
和value
需要和struct
定义时一致。
go
package main
import "fmt"
type Book struct {
Title string // 书名
Pages int // 书的页数
}
func main() {
var book = Book{
Title: "Go Programming Language",
Pages: 10,
}
fmt.Println(book)
var book2 = Book{
Title: "哈哈哈",
}
fmt.Println("Pages", book2.Pages, "Title", book2.Title)
}
未指定值的key
,会默认设置值为零值。如上book2.Pages
未指定,值为0
类(方法与继承)
go
package main
import "fmt"
// 基类(父类)
type Animal struct {
Name string
Age int
}
func (a *Animal) Speak() {
fmt.Printf("%s 发出声音\n", a.Name)
}
func (a *Animal) Eat() {
fmt.Printf("%s 在吃东西\n", a.Name)
}
// 派生类(子类) - 通过组合实现继承
type Dog struct {
Animal // 嵌入 Animal,相当于继承
Breed string // 子类特有属性
}
// 重写父类方法
func (d *Dog) Speak() {
fmt.Printf("%s 汪汪叫\n", d.Name)
}
// 子类特有方法
func (d *Dog) Fetch() {
fmt.Printf("%s 在接飞盘\n", d.Name)
}
func main() {
dog := Dog{
Animal: Animal{
Name: "旺财",
Age: 2,
},
Breed: "金毛",
}
dog.Speak() // 调用重写的方法
dog.Eat() // 调用继承的方法
dog.Fetch() // 调用子类特有方法
fmt.Printf("品种: %s\n", dog.Breed)
}
如上func (d *Dog) Speak() {}
实现类的方法,类似于js es5对原型方法的实现。
继承则是通过内嵌的方式实现。
组合实现继承:是最近比较火的技术理念。比如react hooks也是采用的该理念,近年来的新兴语言go、rust等也是采用该理念实现继承。
无零值
如果期望无零值,可以将属性设置为指针类型:
go
package main
import "fmt"
type Book struct {
Map1 map[string]string
Map2 *map[string]string
}
func main() {
var book = Book{}
fmt.Println(book)
}
如下打印出来无零值:

if
if语法如下:
arduino
if boolean_expression {
// 新分支
}
if,else if, else
示例代码:
go
package main
import "fmt"
func main() {
var arr = [5]int{1, 2, 3, 4, 5}
for _, v := range arr {
fmt.Println("value", v)
if v == 1 {
println("11111")
} else if v == 2 {
println("22222")
} else {
println("其他取值")
}
}
}
同时if也支持嵌套使用,就不再演示了。
注意点:与js不同点
- if 表达式不需要括号
- 相等判断使用
==
- _表示不需要使用该参数名
if表达式中支持声明变量
如下代码:
go
package main
func main() {
if a, c := 1, 2; a > 0 {
println(a)
} else if b := 2; b > 0 {
println(a, b)
} else {
println(a, b, c)
}
}
for循环
for循环在上面已经提到了一些,使用形式为: for 赋值表达式;循环条件;结束后执行表达式
,和js类似,这三个值都可不提供
注意:中间值不提供,相当于结果一直为true,会死循环
go
package main
func main() {
for {
println("xxxx")
}
}
continue和break
continue和break和js的一样,也支持直接跳出到指定label
continue
css
func main() {
var sl = [][]int{
{1, 34, 26, 35, 78},
{3, 45, 13, 24, 99},
{101, 13, 38, 7, 127},
{54, 27, 40, 83, 81},
}
outerloop:
for i := 0; i < len(sl); i++ {
for j := 0; j < len(sl[i]); j++ {
if sl[i][j] == 13 {
fmt.Printf("found 13 at [%d, %d]\n", i, j)
continue outerloop
}
}
}
}
break
go
func main() {
var sl = []int{5, 19, 6, 3, 8, 12}
var firstEven int = -1
// 找出整型切片sl中的第一个偶数
for i := 0; i < len(sl); i++ {
if sl[i]%2 == 0 {
firstEven = sl[i]
break
}
}
println(firstEven) // 6
}
循环中的"坑"
参与循环的是 range 表达式的副本
看如下例子:
go
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("original a =", a)
for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}
打印记过如下:

我们原以为在第一次迭代过程,也就是 i = 0 时,我们对 a 的修改 (a[1] =12,a[2] = 13) 会在第二次、第三次迭代中被 v 取出,但从结果来看,v 取出的依旧是 a 被修改前的值:2 和 3。为什么会是这种情况呢?原因就是参与 for range 循环的是 range 表达式的副本。也就是说,在上面这个例子中,真正参与循环的是 a 的副本,而不是真正的 a。
switch
示例代码
go
package main
func readByExtBySwitch(ext string) {
switch ext {
case "json":
println("read json file")
case "jpg", "jpeg", "png", "gif":
println("read image file")
case "txt", "md":
println("read text file")
case "yml", "yaml":
println("read yaml file")
case "ini":
println("read ini file")
default:
println("unsupported file extension:", ext)
}
}
func main() {
readByExtBySwitch("txt")
}
用法上和js无差别,但是有一点不同,case后面不需要使用break:
go
package main
func main() {
var a = 10
switch true {
case a > 0:
println("a > 0")
case a > 5:
println("a > 5")
default:
println("default case")
}
}
如上,即使两个条件都满足,没有break的情况下,也只会执行第一个。 可通过添加fallthrough
关键词来匹配所有的case:
go
package main
func main() {
var a = 10
switch true {
case a > 0:
println("a > 0")
fallthrough
case a > 5:
println("a > 5")
fallthrough
default:
println("default case")
}
}
临时变量
和 if、for 等控制结构语句一样,switch 语句的 initStmt 可用来声明只在这个 switch 隐式代码块中使用的变量,这种就近声明的变量最大程度地缩小了变量的作用域。
多个case可合并
可以使用,进行多条件合并:
go
package main
func main() {
var a = 3
switch true {
case a > 1, a > 2:
println("a > 0")
case a > -1:
println("a > 5")
default:
println("default case")
}
}
type switch
switch专属用法,可以判断变量类型:
go
package main
func main() {
var x interface{} = 13
switch v := x.(type) {
case nil:
println("v is nil")
case int:
println("the type of v is int, v =", v)
case string:
println("the type of v is string, v =", v)
case bool:
println("the type of v is bool, v =", v)
default:
println("don't support the type")
}
}
如上代码几点说明:
- interface{}类似于ts的any类型。
v :=
, 和 if、for 等控制结构语句一样,switch 语句的 initStmt 可用来声明只在这个 switch 隐式代码块中使用的变量,这种就近声明的变量最大程度地缩小了变量的作用域。x.(type)
为switch
专属用法,可以匹配变量类型,注意:v的值还是 x的值本身,而不是type。
函数
函数我们在上面的例子就接触过了(main
函数和init
函数)。 函数声明语法如下:
func 函数名(参数) (返回值1, ...)
- 返回值可以是任意个。
- 参数也可以是任意个。
- 如果想被其他文件/包使用,函数名必须大写字母开头。
一般官方包/三方包函数返回值都会包含err作为返回值的,通过err== nil
来判断函数执行是否异常
一等公民
类似于js的函数,函数可以作为变量、函数参数、函数返回值。
作为变量
go
var (
myFprintf = func(w io.Writer, format string, a ...interface{}) (int, error) {
return fmt.Fprintf(w, format, a...)
}
)
func main() {
fmt.Printf("%T\n", myFprintf) // func(io.Writer, string, ...interface {}) (int, error)
myFprintf(os.Stdout, "%s\n", "Hello, Go") // 输出Hello,Go
}
作为返回值
go
package main
func setup(task string) func() {
println("do some setup stuff for", task)
return func() {
println("do some teardown stuff for", task)
}
}
func main() {
teardown := setup("demo")
// zheli defer可以去掉
defer teardown()
println("do some bussiness stuff")
}
defer类似 js script defer的意义,defer 关键词后面的函数,会在函数return的时候执行。(执行逻辑go内部控制)
闭包
类似于js的闭包,可以实现柯里化
go
func partialTimes(x int) func(int) int {
return func(y int) int {
return times(x, y)
}
}
作为参数
swift
func greeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome, Gopher!\n")
}
func main() {
http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}
如上,一个http请求,函数作为另一个函数的实参使用。
goroutine
Go 并没有使用操作系统线程作为承载分解后的代码片段(模块)的基本执行单元,而是实现了 goroutine 这一由 Go 运行时(runtime)负责调度的、轻量的用户级线程,为并发程序设计提供原生支持。
一般会认为goroutine就是go语言提供的"协程"。我个人理解,就是一个线程池,类似于nodejs内部的线程池(nodejs io 会使用线程池),一个任务较多会排队,每个任务会分配给一个线程池的线程,执行完成线程释放。
示例代码:
go
package main
import (
"fmt"
"time"
)
func main() {
// 使用匿名函数启动 goroutine
go func(name string) {
fmt.Printf("Hello, %s!\n", name)
}("World")
// 启动多个 goroutine
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Printf("Goroutine %d\n", n)
}(i)
}
time.Sleep(time.Second)
}
不同于nodejs的子线程,goroutine的打印也会自动捕获。
同步机制
go
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 完成时通知 WaitGroup
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second) // 模拟工作
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
// 启动 5 个 worker goroutine
for i := 1; i <= 5; i++ {
wg.Add(1) // 增加计数器
go worker(i, &wg)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("All workers completed")
}
如上等待所有 goroutine完成,才会往下执行。 sync.WaitGroup
是 Go 语言中用于等待一组 goroutine 完成执行的同步工具。它相当于一个计数器,可以等待多个并发操作完成。
goroutine通讯
可以使用channel进行通讯
go
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
fmt.Printf("Producing: %d\n", i)
ch <- i // 发送数据到 channel
time.Sleep(500 * time.Millisecond)
}
close(ch) // 关闭 channel
}
func consumer(ch <-chan int) {
for num := range ch { // 从 channel 接收数据直到关闭
fmt.Printf("Consuming: %d\n", num)
}
}
func main() {
ch := make(chan int, 3) // 创建带缓冲的 channel
go producer(ch)
go consumer(ch)
time.Sleep(3 * time.Second)
}
共享变量
和所有的支持多线程的语言一样,多线程共享变量,可以用锁。
互斥锁
可以使用互斥锁进行变量保护。
go
package main
import (
"fmt"
"sync"
"time"
)
type Counter struct {
sync.Mutex
value int
}
func (c *Counter) Increment() {
c.Lock() // 加锁
defer c.Unlock() // 函数返回时解锁
c.value++
}
func (c *Counter) Value() int {
c.Lock()
defer c.Unlock()
return c.value
}
func main() {
var wg sync.WaitGroup
counter := Counter{}
// 启动 1000 个 goroutine 并发增加计数器
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("最终值: %d\n", counter.Value()) // 保证是 1000
}
读写锁
go
package main
import (
"fmt"
"sync"
"time"
)
type Config struct {
sync.RWMutex
settings map[string]string
}
func (c *Config) Get(key string) string {
c.RLock() // 读锁
defer c.RUnlock()
return c.settings[key]
}
func (c *Config) Set(key, value string) {
c.Lock() // 写锁
defer c.Unlock()
c.settings[key] = value
}
func (c *Config) GetAll() map[string]string {
c.RLock()
defer c.RUnlock()
// 返回副本,避免外部修改内部数据
result := make(map[string]string)
for k, v := range c.settings {
result[k] = v
}
return result
}
func main() {
config := Config{
settings: make(map[string]string),
}
var wg sync.WaitGroup
// 启动多个读 goroutine
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 3; j++ {
value := config.Get("server")
fmt.Printf("Reader %d: server = %s\n", id, value)
time.Sleep(100 * time.Millisecond)
}
}(i)
}
// 启动写 goroutine
wg.Add(1)
go func() {
defer wg.Done()
config.Set("server", "127.0.0.1:8080")
fmt.Println("Writer: 设置 server = 127.0.0.1:8080")
time.Sleep(200 * time.Millisecond)
config.Set("server", "localhost:9090")
fmt.Println("Writer: 设置 server = localhost:9090")
}()
wg.Wait()
fmt.Printf("最终配置: %v\n", config.GetAll())
}
以上就是go语言的基本用法,掌握这些基本的语法,90%的业务场景都能覆盖到,更高级用法可自行了解。
使用goframe 接口开发
GoFrame
是一款模块化、高性能的Go
语言开发框架。 如果您想使用Golang
开发一个业务型项目,无论是小型还是中大型项目,GoFrame
是您的不二之选。如果您想开发一个Golang
组件库,GoFrame
提供开箱即用、丰富强大的基础组件库也能助您的工作事半功倍。 如果您是团队Leader
,GoFrame
丰富的资料文档、详尽的代码注释、 活跃的社区特点将会极大降低您的指导成本,支持团队快速接入、语言转型与能力提升。
安装gf
bash
go install github.com/gogf/gf/cmd/gf/v2@latest
安装不成功,可以参考文档
运行gf -v
输出如下表示安装成功:

脚手架创建项目
gf init gf-chat
启动dev服务
gf run main.go
控制台可以看到:gf自动帮我们启动了swagger接口文档,也有hello 路由。

访问接口

更改端口
如果想更改端口,可以在manifest/config/config.yaml
配置文件中对端口进行更改,当然也可以改log和数据库相关的配置。
注意:该配置只对dev环境下生效,生产环境需要和go build产物在同一个目录下面
代码详解:
main.go
go
package main
import (
_ "gf-chat/internal/packed"
"github.com/gogf/gf/v2/os/gctx"
"gf-chat/internal/cmd"
)
func main() {
cmd.Main.Run(gctx.GetInitCtx())
}
main.go代码比较简单: 就是运行了cmd.Main.Run
主要的逻辑都在gf-chat/internal/cmd
文件
go
package cmd
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
"gf-chat/internal/controller/hello"
)
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(
hello.NewV1(),
)
})
s.Run()
return nil
},
}
)
s := g.Server()
创建一个服务对象
s.Group
创建路由组,group里面我们可以做两件事: 1 创建统一路由前缀。 2 group里面可以做一些统一的事情(在回调函数里面)。例如上面的例子就是先将请求交由中间价处理,再匹配其他路由。
有些接口需要鉴权有些不需要,我们可以通过group分离。统一的鉴权、加密/解密、数据权限控制等我们都可以放到中间件里面去处理。
再看 internal/controller/hello
结合我们之前讲的继承与package name的知识
实际上处理请求的文件为internal/controller/hello/hello_v1_hello.go
scss
func (c *ControllerV1) Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) {
g.RequestFromCtx(ctx).Response.Writeln("Hello World!")
return
}
更多细节可以参考goframe官方文档。官方文档写的很详细,推荐阅读。
实现一个CRUD
接下来我们实现一个user模块的增删改查 (了解了这些就可以用go 愉快的开发接口了。)
运行mysql
如果你本地已经运行mysql服务,或者已经连接其他远程的数据库。可不安装。
docker的方式启动mysql:
bash
docker run -d --name mysql \
-p 3306:3306 \
-e MYSQL_DATABASE=test \
-e MYSQL_ROOT_PASSWORD=12345678 \
loads/mysql:5.7
数据库连接配置
有两个文件可以配置数据库连接:
manifest/config/config.yaml
"运行时"的配置,实现CRUD
数据库操作时,数据库连接操作。hack/config.yaml
主要用于自动生成代码(比如表数据和model的字段映射),下文会提到的。
如果数据和gf默认的配置不一致,需要两个配置文件都修改连接配置(host/账号/密码/数据库等):
arduino
# https://goframe.org/docs/core/gdb-config-file
database:
default:
link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
创建表user
执行下面的sql语句创建用户表。
sql
CREATE TABLE
`user` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`password` varchar(255) NOT NULL COMMENT 'md5 密码',
`role` int NOT NULL COMMENT '角色 0 root 1 组管理员 2 普通项目管理 可以管理多个',
`username` varchar(255) NOT NULL COMMENT '用户名登录名',
`nickname` varchar(255) NOT NULL COMMENT '昵称',
`salt` varchar(255) NOT NULL,
`phone` varchar(255) NOT NULL COMMENT '钉钉手机号方便消息通知',
PRIMARY KEY (`id`) ) ENGINE = InnoDB AUTO_INCREMENT = 28 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci
自动生成代码
数据库映射(model)
执行 make dao
,会按照我们的数据库表, 每张表将会生成三类Go
文件:
dao
:通过对象方式访问底层数据源,底层基于ORM
组件实现。do
:数据转换模型,用于业务模型到数据模型的转换,由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。entity
:数据模型,由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。
这里我们只需要明确一点:把数据库的表、字段和我们的go struct做映射,感兴趣的可以自行了解细节。
生成CRUD接口
编写接口定义(ps:接口定义编写好了,会自动接口handler文件)。
api/user/v1/user.go
:
c
package v1
import (
v1 "gf-chat/api/no_auth/v1"
"gf-chat/internal/model/entity"
"github.com/gogf/gf/v2/frame/g"
)
type UserCreateReq struct {
g.Meta `path:"/user" method:"post" tags:"user" summary:"create user request"`
Username string `v:"required" dc:"用户名"`
Nickname string `v:"required" dc:"昵称"`
Password string `v:"required" dc:"密码"`
Role int `v:"required|in:1,2"`
Phone string `v:"required" json:"phone" dc:"手机号"`
}
type UserCreateRes struct {
}
type UserUpdateReq struct {
g.Meta `path:"/user/{id}" method:"put" tags:"user" summary:"create user request"`
Nickname string `v:"required" dc:"昵称"`
Id int64 `v:"required" dc:"user id"`
Role int `v:"required|in:1,2"`
Phone string `v:"required" json:"phone" dc:"手机号"`
}
type UserUpdateRes struct {
*entity.User
Password string `json:"-"` // 这个字段将被忽略
}
type GetUserListReq struct {
g.Meta `path:"/user" method:"get" tags:"user" summary:"Get user list"`
Search string `v:"length:1,10" dc:"search user"`
}
type GetUserListRes struct {
List []*v1.SafeUser `json:"list" dc:"user list"`
}
api/no_auth/v1/no_auth.go
csharp
package v1
// 不需要登录校验的接口
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
type UserLoginReq struct {
g.Meta `path:"/user/login" method:"post" tags:"user" summary:"create user request"`
Username string `v:"required" dc:"用户名"`
Password string `v:"required" dc:"密码"`
}
type SafeUser struct {
Id uint `json:"id" orm:"id" description:""` //
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` //
Role int `json:"role" orm:"role" description:"角色 0 root 1 组管理员 2 普通项目管理 可以管理多个"` // 角色 0 root 1 组管理员 2 普通项目管理 可以管理多个
ProjectList string `json:"projectList" orm:"project_list" description:"json数组"` // json数组
Username string `json:"username" orm:"username" description:"用户名登录名"` // 用户名登录名
Nickname string `json:"nickname" orm:"nickname" description:"昵称"` // 昵称
Phone string `json:"phone" dc:"手机号"`
}
type UserLoginRes struct {
Token string `json:"token"`
User *SafeUser `json:"user"`
}
自动生成代码: 执行make ctrl
如图会生成接口文件:

完成接口逻辑:
internal/controller/user/user_v1_user_create.go
go
// 注册用户
func (c *ControllerV1) UserCreate(ctx context.Context, req *v1.UserCreateReq) (res *v1.UserCreateRes, err error) {
// 用户名是否存在 存在就是异常
existData := &entity.User{}
_ = dao.User.Ctx(ctx).Where("username", req.Username).Unscoped().Scan(&existData)
if existData.Id != 0 {
return nil, gerror.New("user already exists")
}
// salt := grand.S(16)
// 密码可以加密存储,加密密码 (MD5(密码+盐值))
// encryptedPwd := consts.EncryptPassword(req.Password, salt)
_, err = dao.User.Ctx(ctx).Data(do.User{
Username: req.Username,
Password: req.Password,
Salt: salt,
Nickname: req.Nickname,
Role: req.Role,
Phone: req.Phone,
}).Insert()
res = &v1.UserCreateRes{}
if err != nil {
// 最好不要直接return err
return nil, err
}
return
}
代码讲解:
1 dao.User
user表的Model,gf给我们内置的ORM model。 2 Ctx(ctx)
,ctx是go语言核心概念,比较类似nodejs的koa框架中ctx的概念和用法。可以在请求链中传递元数据。 3 Where
就是sql的where语句。多个条件需要写多个Where
4 .Unscoped()
如前文所说,三方库的函数,多个返回值会有err,查询不到记录err就不为nil,Unscoped就是不捕获该错误。有点像ts的"!"断言(数据一定存在)。 5 Scan(&existData)
将sql语句查询结果写入到&existData中,&existData相当于是existData的内存地址。 6 _ =
表示我们不需要使用函数的返回结果,因为使用了scan操作,返回值不存在的。 7 UserCreate的返回值如果为空说明无异常,返回nil和err说明有错误,最好不要直接return err,因为有些sql执行的err直接返回给前端和用户不是很友好。
以上代码演示包含了数据库查询与插入。其他的操作就不再演示。
路由注册
internal/cmd/cmd.go
go
package cmd
import (
"context"
"gf-chat/internal/controller/no_auth"
"gf-chat/internal/controller/user"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
)
// 跨域中间件
func MiddlewareCORS(r *ghttp.Request) {
r.Response.CORSDefault() // 使用默认跨域配置(允许所有来源)
r.Middleware.Next()
}
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
s := g.Server()
s.Group("/api", func(group *ghttp.RouterGroup) {
group.Middleware(MiddlewareCORS) // 注册中间件
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(user.NewV1())
})
s.Group("/api", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(no_auth.NewV1())
})
s.Run()
return nil
},
}
)
代码说明:
- package name
v1
以及目录包含v1
,是一个比较好的命名规范,后续版本升级什么的,机构比较清晰和可维护 - 对于用户信息,查询时不能将密码等字段给用户返回
- 如果是我们的
struct
(比如type SafeUser struct
)数据直接给用户返回,会是大写开头的字段key,如果想要自定义,则需要修改json key映射
c
Username string `json:"username" orm:"username" description:"用户名登录名"` // 用户名登录名
接入langchain
LangChain 是一个用于开发大语言模型(LLM)应用程序的框架。它提供了一套工具、组件和接口,简化了构建基于 LLM 的应用程序的过程。
核心思想
-
链式(Chaining):将多个组件连接起来形成工作流
-
组件化:提供可复用的模块化组件
-
上下文管理:有效处理对话历史和上下文
可以把langchain简单的理解为和大模型交互的协议,在这个协议的基础上有很多解决方案组件,比如网络搜索、历史对话、rag等。比较像k8s,我们只要写一些配置,各个pod之间的交互、依赖都全部帮我们处理好了。
ollama安装
推荐使用安装客户端 安装包下载地址:ollama.com/download/ma...
安装完成打开ollama客户端,能识别ollama
命令说明安装成。
安装模型
选择你想安装的模型(官网)
这里我选择安装 llama3.1
bash
ollama pull llama3.1
查看安装的模型列表:
bash
ollama list
安装langchain go 版本sdk
bash
go get github.com/tmc/langchaingo
go get github.com/tmc/langchaingo/llms@v0.1.13
测试代码
ollama.go
go
package main
import (
"context"
"fmt"
"log"
"github.com/tmc/langchaingo/llms/ollama"
)
func main() {
// 创建 Ollama LLM 实例
llm, err := ollama.New(ollama.WithModel("llama3.1"))
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// 简单调用
response, err := llm.Call(ctx, "请解释什么是人工智能?")
if err != nil {
log.Fatal(err)
}
fmt.Println("回答:", response)
}
运行:
go
go run ollama.go
运行结果:
go
go run ollama.go
回答: 人工智能(Artificial Intelligence, AI)是一种模拟人类思维和行为的计算机系统。它通过学习、推理和解决问题来实现自主决策和执行任务。
AI助手实现
AI助手实现起来因为比较复杂,计划在下一篇博客写该项目。
下篇博客包含内容:
- SSE接口实现,对话通讯协议,比如需要创建sessioncode(每次新建对话)和chatcode(用户每输入一次query)。
- 前后端卡片协议。一般在ai对话功能实现中,前后端需要定义一套卡片协议,比如大模型返回富文本、图片、视频等内容,前端需要承接。消息状态(解析中、文本输出中、输出结束、错误卡片等)。
- langchain功能对接,历史对话传递。实现网络搜索、rag、token压缩等。