目标筑基:从0到1学习GoLang (入门 Go语言+GoFrame开发服务端+ langchain接入)

前言

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,keyvalue需要和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")
    }
}

如上代码几点说明:

  1. interface{}类似于ts的any类型。
  2. v :=, 和 if、for 等控制结构语句一样,switch 语句的 initStmt 可用来声明只在这个 switch 隐式代码块中使用的变量,这种就近声明的变量最大程度地缩小了变量的作用域。
  3. x.(type)switch专属用法,可以匹配变量类型,注意:v的值还是 x的值本身,而不是type。

函数

函数我们在上面的例子就接触过了(main函数和init函数)。 函数声明语法如下:

func 函数名(参数) (返回值1, ...)

  1. 返回值可以是任意个。
  2. 参数也可以是任意个。
  3. 如果想被其他文件/包使用,函数名必须大写字母开头。

一般官方包/三方包函数返回值都会包含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 提供开箱即用、丰富强大的基础组件库也能助您的工作事半功倍。 如果您是团队 LeaderGoFrame 丰富的资料文档、详尽的代码注释、 活跃的社区特点将会极大降低您的指导成本,支持团队快速接入、语言转型与能力提升。

官方文档

安装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
数据库连接配置

有两个文件可以配置数据库连接:

  1. manifest/config/config.yaml "运行时"的配置,实现CRUD数据库操作时,数据库连接操作。
  2. 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助手实现起来因为比较复杂,计划在下一篇博客写该项目。

下篇博客包含内容:

  1. SSE接口实现,对话通讯协议,比如需要创建sessioncode(每次新建对话)和chatcode(用户每输入一次query)。
  2. 前后端卡片协议。一般在ai对话功能实现中,前后端需要定义一套卡片协议,比如大模型返回富文本、图片、视频等内容,前端需要承接。消息状态(解析中、文本输出中、输出结束、错误卡片等)。
  3. langchain功能对接,历史对话传递。实现网络搜索、rag、token压缩等。
相关推荐
间彧几秒前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧6 分钟前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧7 分钟前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧9 分钟前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧13 分钟前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧18 分钟前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
冴羽31 分钟前
今日苹果 App Store 前端源码泄露,赶紧 fork 一份看看
前端·javascript·typescript
蒜香拿铁33 分钟前
Angular【router路由】
前端·javascript·angular.js
brzhang1 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构
西洼工作室1 小时前
高效管理搜索历史:Vue持久化实践
前端·javascript·vue.js