目标筑基:从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压缩等。
相关推荐
桜吹雪2 小时前
15 个可替代流行 npm 包的 Node.js 新特性
javascript·后端
笃行3502 小时前
KingbaseES SQL Server模式扩展属性管理:三大存储过程实战指南
后端
温宇飞2 小时前
CSS 属性分类
前端
鹏多多2 小时前
使用React-OAuth进行Google/GitHub登录的教程和案例
前端·javascript·react.js
SimonKing2 小时前
继老乡鸡菜谱之后,真正的AI菜谱来了,告别今天吃什么的烦恼...
java·后端·程序员
晓得迷路了3 小时前
栗子前端技术周刊第 101 期 - React 19.2、Next.js 16 Beta、pnpm 10.18...
前端·javascript·react.js
Java技术小馆3 小时前
AI模型统一接口桥接工具
后端·程序员
玲小珑3 小时前
LangChain.js 完全开发手册(十四)生产环境部署与 DevOps 实践
前端·langchain·ai编程
亿元程序员3 小时前
有了AI,游戏开发新人还有必要学Cocos游戏开发吗?
前端