前言
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压缩等。