跳到主要内容

Linux Ollama 常用命令和 API 调用

· 阅读需 2 分钟
素明诚
Full stack development

安装 ollama

curl -fsSL https://ollama.com/install.sh | sh

19129b550cdfe1a30a05973ac4fccb9b## 下载模型

下载 gemma 进行测试,下次成功后会直接进入跟模型的交互模式。可以直接在命令行聊天

ollama run gemma:2b

你也可以下载自己需要的 https://ollama.com/library

查看模型

root@gptdev:/home# ollama list
NAME ID SIZE MODIFIED
gemma:2b b50d6c999e59 1.7 GB 24 minutes ago

查看模型详情

root@gptdev:/home# ollama show gemma:2b
Model
arch gemma
parameters 3B
quantization Q4_0
context length 8192
embedding length 2048

Parameters
repeat_penalty 1
stop "<start_of_turn>"
stop "<end_of_turn>"

License
Gemma Terms of Use
Last modified: February 21, 2024

删除模型

ollama rm <model-name>

运行模型

root@gptdev:/home# ollama run gemma:2b

9e5360abeb936307f17341381d6658e1## 退出交互模式

Use Ctrl + d or /bye to exit.
>>> /bye

使用 API 调用

generate 一次性回答

root@gptdev:/home# curl -XPOST http://localhost:11434/api/generate -d '{"model": "gemma:2b", "prompt": "星星为什么是一闪一闪的?", "stream": false}'

回答↓
{
"model": "gemma:2b",
"created_at": "2024-07-12T08:35:18.532841683Z",
"response": "星星不是一闪一闪的。星星是光源,是通过核反应产生光子的天体。",
"done": true,
"done_reason": "stop",
"context": [],
"total_duration": 3780790485,
"load_duration": 41295298,
"prompt_eval_count": 33,
"prompt_eval_duration": 134544000,
"eval_count": 23,
"eval_duration": 3562200000
}

聊天

root@gptdev:/home# curl http://localhost:11434/api/chat -d '{
"model": "gemma:2b",
"messages": [
{ "role": "system", "content": "You are an astronomer. Answer the following questions based on your knowledge of astronomy." },
{ "role": "user", "content": "星星为什么是一闪一闪的?中文回答~" }
],
"stream": false
}

回答↓
{
"model": "gemma:2b",
"created_at": "2024-07-12T08:39:00.414947479Z",
"message": {
"role": "assistant",
"content": "星星是由于其表面反射的灯光造成的。 这是因为星光的波长比地球的光波长更长,这意味着当它从遥远的星系中进入地球时,它被地球表面吸收的更多光线。 这使得我们可以看到的那部分光被反射回来,我们就能看到星星。\n\n星星之所以闪闪是因为它们非常光亮。 这意味着它们发射的光线非常强,可以照亮整个地球表面。 当我们看到星星时,我们看到的是它们反射回来的光。\n\n星星是宇宙中最亮的天体,可以见到的最远的天体。"
},
"done_reason": "stop",
"done": true,
"total_duration": 21237384982,
"load_duration": 45647661,
"prompt_eval_count": 54,
"prompt_eval_duration": 372113000,
"eval_count": 125,
"eval_duration": 20685706000
}

个人觉得 Ollama 真心挺不错的,让跑大模型这件事变的简单了,专心搞业务就行了。

什么是服务发现

· 阅读需 2 分钟
素明诚
Full stack development

服务注册和服务查找

服务注册

每个微服务在启动时主动向服务注册中心注册自己的关键信息,如 IP 地址、端口号、服务名称等。这个过程确保服务的可发现性,并在服务状态改变时更新注册信息。

服务查找

当一个服务需要与另一服务交互时,它通过服务注册中心查询所需服务的可用实例和地址信息。这个查询依赖于服务注册数据。

Go 语言实现服务发现

Consul:Consul 是一个提供服务发现、配置和分段功能的工具,它通过一个简单的 HTTP API 为服务注册和服务发现提供支持。Go 可以通过 HashiCorp 提供的 Consul API 客户端库与 Consul 交互

Etcd:Etcd 是一个键值存储仓库,用于配置共享和服务发现。Etcd 的客户端库为 Go 语言提供了接口,可以很方便地集成到 Go 微服务中

Eureka:虽然 Eureka 主要是 Netflix 开发并且与 Java 生态系统结合更紧密,但也有 Go 客户端可以与 Eureka 服务注册和发现机制交互。

Go 语言微服务示例

Go 中使用 Consul 进行服务注册和发现

package main

import (
"github.com/hashicorp/consul/api"
"log"
)

func main() {
// 创建一个 Consul 客户端
client, err := api.NewClient(api.DefaultConfig())
if err != nil {
log.Fatal(err)
}

// 服务注册
registration := new(api.AgentServiceRegistration)
registration.ID = "service1"
registration.Name = "userservice"
registration.Port = 8080
registration.Address = "127.0.0.1"
client.Agent().ServiceRegister(registration)

// 服务发现
services, err := client.Agent().Services()
if err != nil {
log.Fatal(err)
}
for _, service := range services {
log.Println("Service:", service.Service, "Address:", service.Address, "Port:", service.Port)
}
}

Consul 功能详解实践服务注册和发现案例

· 阅读需 4 分钟
素明诚
Full stack development

服务发现

服务实例在启动时向 Consul 注册其详细信息(如服务名、IP、端口),并通过 Consul 查询其他服务的详细信息,实现动态发现和连接。

定期健康检查:配置健康检查确保仅健康实例被发现。

服务标签:利用标签对服务进行分类,便于管理和过滤。

健康检查

可以设置多种类型的健康检查,如 HTTP、TCP、Docker 或自定义脚本,以验证服务状态。

精细的检查间隔:根据服务的重要程度调整检查频率。你的服务挂了超过多久自动删除

键值存储

通过 Consul 的 HTTP API 进行键值对的读写操作,存储配置数据或其他共享数据。

动态配置更新:利用键值存储动态更新配置,无需重启服务。

数据一致性:使用 Consul 的事务功能确保关键配置的一致性更新。

多数据中心支持

Consul 允许配置跨多个地理区域的服务同步和状态共享,每个数据中心都可以独立运行,同时与其他数据中心保持同步。

地理特定的健康检查:每个数据中心应有针对性的健康检查策略。

加密通信:确保所有数据中心间的通信经过加密,保护数据安全。

服务发现和注册案例

启动 A 和 B 两个服务之前需要保证 Consul 是正常运行的,并且 Consul 和服务直接的网络是相通的

配置文件

consul:
address: "http://172.22.220.64"
port: "8500"

服务 a

package Consul

import (
"github.com/gin-gonic/gin"
"github.com/hashicorp/consul/api"
"github.com/spf13/viper"
"log"
"net/http"
)

func init() {
viper.SetConfigFile("./Consul/config.yaml")
err := viper.ReadInConfig() // 查找并读取配置文件
if err != nil { // 处理读取配置文件的错误
log.Fatalf("致命错误: 配置文件读取失败: %s \n", err)
}
}

func main() {

// 创建 Consul 客户端
consulConfig := api.DefaultConfig()
consulConfig.Address = viper.GetString("consul.address") + ":" + viper.GetString("consul.port")
consulClient, err := api.NewClient(consulConfig)
if err != nil {
log.Fatalf("创建 Consul 客户端时发生错误: %s", err)
}

// 注册服务
registration := &api.AgentServiceRegistration{
ID: "serviceA",
Name: "service-a",
Address: "172.16.50.251",
Port: 8080,
Check: &api.AgentServiceCheck{
HTTP: "http://172.16.50.251:8080/health",
Interval: "10s",
DeregisterCriticalServiceAfter: "1m",
},
}
err = consulClient.Agent().ServiceRegister(registration)
if err != nil {
log.Fatalf("向 Consul 注册服务 A 时发生错误: %s", err)
}

// 设置 Gin 路由
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.Status(http.StatusOK)
})
r.GET("/data", func(c *gin.Context) {
c.String(http.StatusOK, "来自服务 A 的问候!")
})

// 启动 HTTP 服务器
log.Println("服务 A 正在端口 8080 上运行")
r.Run(":8080")
}


服务 b

package Consul

import (
"github.com/gin-gonic/gin"
"github.com/hashicorp/consul/api"
"github.com/spf13/viper"
"io/ioutil"
"log"
"net/http"
"strconv"
)

func init() {
viper.SetConfigFile("./Consul/config.yaml")
err := viper.ReadInConfig()
if err != nil {
log.Fatalf("致命错误: 配置文件读取失败: %s \n", err)
}
}

func main() {
// 创建 Consul 客户端
consulConfig := api.DefaultConfig()
consulConfig.Address = viper.GetString("consul.address") + ":" + viper.GetString("consul.port")
consulClient, err := api.NewClient(consulConfig)
if err != nil {
log.Fatalf("创建 Consul 客户端时发生错误: %s", err)
}

// 注册服务
registration := &api.AgentServiceRegistration{
ID: "serviceB",
Name: "service-b",
Address: "172.16.50.251",
Port: 8081,
Check: &api.AgentServiceCheck{
HTTP: "http://172.16.50.251:8081/health",
Interval: "10s",
DeregisterCriticalServiceAfter: "1m",
},
}
err = consulClient.Agent().ServiceRegister(registration)
if err != nil {
log.Fatalf("向 Consul 注册服务 B 时发生错误: %s", err)
}

// 设置 Gin 路由
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.Status(http.StatusOK)
})
r.GET("/call-a", func(c *gin.Context) {
// 发现服务 A 并调用其 API
services, _, err := consulClient.Health().Service("service-a", "", true, nil)
if err != nil {
c.String(http.StatusInternalServerError, "从 Consul 获取服务时发生错误")
return
}
if len(services) > 0 {
serviceA := services[0].Service
url := "http://" + serviceA.Address + ":" + strconv.Itoa(serviceA.Port) + "/data"
resp, err := http.Get(url)
if err != nil {
c.String(http.StatusInternalServerError, "调用服务 A 时发生错误")
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
c.String(http.StatusInternalServerError, "读取服务 A 响应时发生错误")
return
}
c.String(http.StatusOK, string(body))
} else {
c.String(http.StatusNotFound, "未找到服务 A")
}
})

// 启动 HTTP 服务器
log.Println("服务 B 正在端口 8081 上运行")
r.Run(":8081")
}


测试

package test

import (
"io/ioutil"
"net/http"
"testing"
)

func TestService_b(t *testing.T) {
// 直接发起对服务 A 的 GET 请求
resp, err := http.Get("http://172.16.50.251:8081/call-a")
if err != nil {
t.Fatal("创建请求失败:", err)
}
defer resp.Body.Close()

// 检查状态码
if resp.StatusCode != http.StatusOK {
t.Errorf("期望的状态码 200,但是获取的是 %d", resp.StatusCode)
}

// 读取响应体
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal("读取响应体失败:", err)
}
body := string(bodyBytes)

// 打印响应体内容
t.Logf("响应体内容: %s", body)
}


5a33480b91f5d14e7b6c3c441d395a5f

求个赞

Git 保留主分支的最近一次提交放弃之前所有的提交

· 阅读需 1 分钟
素明诚
Full stack development

切换到主分支: 确保你当前处于主分支

git checkout main

重置分支到最新的提交: 使用 git reset 命令将分支重置到最新的提交这里使用 --hard 选项,这将会重置工作目录和索引(暂存区)到你指定的提交

git reset --hard HEAD

删除旧的提交记录: 如果你想彻底删除除最新之外的所有提交记录,可以使用下面的步骤

警告:这将不可逆转地删除这些提交记录

git branch new-temp-branch $(git commit-tree HEAD^{tree} -m "New initial commit")
git checkout new-temp-branch
git branch -D main
git branch -m main

清理仓库: 运行垃圾回收命令来清理不再需要的对象(即旧的提交记录)

git gc --prune=now

推送变更到远程仓库: 如果你的分支是跟踪远程仓库的,你需要强制推送这些变更,覆盖之前所有的历史记录

git push origin main --force

Go 全局配置的四种方式

· 阅读需 2 分钟
素明诚
Full stack development

常量 (Constants)

对于不会改变的配置项,常量是最好的选择

package config

// 定义全局常量
const (
ServerPort = 8080
DatabaseURI = "localhost:5432"
Environment = "production"
)

Map

如果需要运行时可以改变的配置项,Map结构体更好

package config

// 使用map来存储配置
var GlobalConfig = map[string]interface{}{
"timeout": 30,
"logLevel": "info",
"maxConnections": 100,
}

func init() {
// 这里可以根据不同环境或其他逻辑动态调整配置
if Environment == "development" {
GlobalConfig["logLevel"] = "debug"
}
}

Flag

Flag 适用于需要由最终用户在程序启动时配置的参数

package main

import (
"flag"
"fmt"
)

var (
port = flag.Int("port", 8080, "Server port number")
logLevel = flag.String("logLevel", "info", "Logging level")
)

func main() {
flag.Parse() // 解析传入的命令行参数
fmt.Printf("Server will start at port: %d with log level: %s\n", *port, *logLevel)
}

通过命令行传入

$ go run program.go -port=9090

结构体(构造实例)

对于需要良好组织的大型应用配置,结构体提供了最佳的类型安全性和可维护性

package config

type Config struct {
Port int
DatabaseURI string
LogLevel string
}

// NewConfig 创建一个新的Config实例
func NewConfig() *Config {
return &Config{
Port: 8080,
DatabaseURI: "localhost:5432",
LogLevel: "info",
}
}

var GlobalConfig = NewConfig()

func init() {
// 可以根据需要在这里调整配置
if Environment == "development" {
GlobalConfig.LogLevel = "debug"
}
}

以上是使用 Go 解决配置文件的方式,实际使用推荐 viper 和 godotenv

Go 的v v v 有什么区别

· 阅读需 1 分钟
素明诚
Full stack development

这里的 v 代表的是 “value”

%v

最基本的格式占位符,用于以默认的方式输出变量的值对于大多数变量类型来说,%v 会输出值的自然形式

%+v

对于结构体(struct)来说,%+v 不仅会输出结构体的值,还会包括结构体的字段名这对于调试或显示更详细的信息是非常有用的

%#v

这个格式占位符会输出值的 Go 语法表示也就是说,输出的格式可以直接作为源代码片段使用这对于调试复杂的数据结构特别有用,因为它可以展示如何使用代码来重建当前的值

这三者的区别如下

package main

import (
"fmt"
)

type Person struct {
Name string
Age int
}

func main() {
p := Person{"John Doe", 30}
fmt.Printf("Using %%v: %v\n", p)
fmt.Printf("Using %%+v: %+v\n", p)
fmt.Printf("Using %%#v: %#v\n", p)
}

输出是

Using %v: {John Doe 30}
Using %+v: {Name:John Doe Age:30}
Using %#v: main.Person{Name:"John Doe", Age:30}

不同的格式占位符对于输出格式有着明显的影响,特别是在处理结构体时

悲观锁和乐观锁的区别

· 阅读需 3 分钟
素明诚
Full stack development

悲观锁和乐观锁是数据库管理和并发编程中用来控制数据一致性防止数据冲突的两种策略

悲观锁(Pessimistic Locking)

  • 核心 悲观锁假设最坏的情况,认为数据在并发修改时一定会发生冲突,因此在数据处理前就锁定数据,直到事务完成才释放锁
  • 应用场景 适用于写操作多、冲突概率高的环境,如银行账户余额更新

乐观锁(Optimistic Locking)

  • 核心 乐观锁假设冲突发生的可能性较低,因此不会立即锁定数据,而是在数据提交更新时检查数据在读取后是否被修改过(通常通过版本号或时间戳实现)
  • 应用场景 适用于读操作多、冲突概率低的环境,如一些只偶尔更新的数据查询系统

为什么会有这两种锁

这两种锁的存在是为了处理并发情况下的数据一致性和系统性能的平衡 悲观锁提供较强的数据一致性保证,但可能降低系统的并发性能;乐观锁在并发性能上表现更好,但在高冲突环境下可能导致更多的重试和失败 选择哪种锁策略取决于系统的具体需求和预期的并发冲突情况

两种锁在 Go 中的实现方式

悲观锁

在 Go 中,悲观锁通常可以通过使用 sync.Mutexsync.RWMutex 来实现 这些锁在访问共享资源前必须先获得锁,并在访问完成后释放锁 这样可以确保同一时间内只有一个 goroutine 能修改该资源

package main

import (
"fmt"
"sync"
)

var (
balance int
mutex sync.Mutex
)

func Deposit(amount int) {
mutex.Lock() // 获取锁
balance += amount // 修改共享资源
mutex.Unlock() // 释放锁
}

func main() {
balance = 100
fmt.Println("Initial balance:", balance)
Deposit(50)
fmt.Println("New balance:", balance)
}

使用 sync.Mutex 来确保在修改账户余额时不会发生数据竞争 这是一种悲观锁的实现,因为它假设必然会有冲突

乐观锁

乐观锁在 Go 中可以通过原子操作或自定义逻辑来实现

package main

import (
"fmt"
"sync"
"sync/atomic"
)

var (
balance int64
)

func Deposit(amount int64) {
for {
oldBalance := atomic.LoadInt64(&balance) // 读取当前余额
newBalance := oldBalance + amount // 计算新余额
// 尝试更新余额,如果期间余额未被其他 goroutine 修改,则更新成功
if atomic.CompareAndSwapInt64(&balance, oldBalance, newBalance) {
return
}
// 如果数据被修改过,循环重试
}
}

func main() {
atomic.StoreInt64(&balance, 100) // 初始余额
fmt.Println("Initial balance:", balance)
Deposit(50)
fmt.Println("New balance:", balance)
}

使用 atomic.CompareAndSwapInt64 函数来确保在更新余额时,如果没有其他 goroutine 修改过余额,则更新操作会成功 如果余额在更新前被其他 goroutine 修改了,则重试操作,直到成功 这是乐观锁的一种实现方式,它假设冲突发生的可能性较低

github 使用个人访问令牌PAT克隆项目

· 阅读需 1 分钟
素明诚
Full stack development

生成 PAT

登录 GitHub。

在右上角点击你的头像,然后选择“Settings(设置)”。

在侧边栏中选择“Developer settings(开发者设置)”。

选择“Personal access tokens(个人访问令牌)”,然后点击“Generate new token(生成新令牌)”。

设置令牌的权限,并生成。

克隆项目

替换为仓库的 URL

git clone https://username@repository-url.git

当提示输入密码时,输入刚才生成的 PAT

数据库规范化 1NF 到 3NF 的详解

· 阅读需 2 分钟
素明诚
Full stack development

第一范式(1NF)

每个表格的列都必须拥有单一的(不可再分的)值,列不能有重复的组

假设有一个记录学生爱好的表格,如果一个学生有多个爱好,这样设计就违反了 1NF

StudentIDStudentNameHobbies
1Alice游泳, 跑步

为了满足 1NF,我们应将“Hobbies”拆分为多行,每行表示一个爱好

StudentIDStudentNameHobby
1Alice游泳
1Alice跑步

第二范式(2NF)

基于 1NF,所有非主键字段必须完全依赖于整个主键

假设一个表格记录学生参加的课程及课程老师,如果主键是(StudentID, CourseID),则下表违反了 2NF,因为“TeacherName”只依赖于“CourseID”部分

StudentIDCourseIDTeacherName
1101Dr. Smith
2101Dr. Smith
1102Dr. Jones

为了达到 2NF,我们应该把“TeacherName”移到单独的表格中

Courses Table

CourseIDTeacherName
101Dr. Smith
102Dr. Jones

Enrollment Table

StudentIDCourseID
1101
2101
1102

第三范式(3NF)

基于 2NF,任何非主键字段不应依赖于其他非主键字段

如果有一个员工表,其中包括员工、他们所在的部门以及部门经理

EmployeeIDDepartmentNameDepartmentManager
E01SalesJohn Doe
E02SalesJohn Doe
E03HRJane Smith

这里,“DepartmentManager”依赖于“DepartmentName”,违反了 3NF 正确的设计应该是创建一个独立的“部门”表

Departments Table

DepartmentNameDepartmentManager
SalesJohn Doe
HRJane Smith

Employees Table

EmployeeIDDepartmentName
E01Sales
E02Sales
E03HR

Go 语言的 init 函数执行顺序

· 阅读需 2 分钟
素明诚
Full stack development

单个 Go 文件中的多个 init 函数

一个 Go 文件内可以包含多个 init 函数,这些函数将按照它们在文件中的出现顺序被执行这一点对于初始化本地资源或配置特别有用

示例代码

// 文件config.go
package config

import "fmt"

func init() {
fmt.Println("初始化数据库连接")
}

func init() {
fmt.Println("加载应用配置")
}

输出结果

初始化数据库连接
加载应用配置

同一 package 中的多个 Go 文件中的 init 函数

如果一个 package 包含多个 Go 文件,那么各文件中的 init 函数将按文件名的字母顺序执行这对于需要跨文件初始化顺序的场景非常关键

示例代码

// 文件a_init.go
package setup

import "fmt"

func init() {
fmt.Println("Setup part 1")
}

// 文件b_init.go
package setup

func init() {
fmt.Println("Setup part 2")
}

输出结果

Setup part 1
Setup part 2

不同 package 的 init 函数执行顺序

当一个 package 导入其他多个 package 时,被导入的 package 的 init 函数将按照 import 语句的顺序执行,再执行当前 package 的 init 函数这有助于管理复杂的依赖关系

示例代码

// 文件database.go
package database

import "fmt"

func init() {
fmt.Println("Database initialized")
}

// 文件server.go
package main

import (
"fmt"
"sumingcheng.com/project/database" // 路径假设为例子
)

func init() {
fmt.Println("Server setup")
}

func main() {
fmt.Println("Server starting")
}

输出结果

Database initialized
Server setup
Server starting

非 main package 的 init 函数执行顺序

即便是在非 main package 中,init 函数也是在包的任何其他代码执行前执行,包括 main 函数这保证了无论程序的入口点在哪里,所有必要的初始化都能先行完成

示例代码

// 文件logger.go
package logger

import "fmt"

func init() {
fmt.Println("Logger setup complete")
}

// 文件main.go
package main

import (
"sumingcheng.com/project/logger" // 假设的路径
)

func main() {
fmt.Println("Main function running")
}

输出结果

Logger setup complete
Main function running