gorm写入零值问题

作者:matrix 发布时间:2024-10-21 分类:Golang

gorm中如果数据为结构体类型的零值时,不会触发零值更新。

比如当前有表结构体

type OrderTemplate struct {
    ID           int    `gorm:"primaryKey;autoIncrement;column:id" json:"id"`
    InputType    int    `gorm:"not null;default:1;column:input_type" json:"input_type"`       
}

注意:
InputType类型为 int,设置了default为 1

当数据传入 0 ,是 int 类型的默认零值0,会认为你没有传入所以就用default值去填充。如果这里不配置default,系统也就不会写入数据。

解决方案

结构体字段使用非int类型,如 sql.NullInt64,*int指针

简单点建议直接使用指针类型。使用sql.NullInt64类型传入时需要注意设置Valid为 true

sql.NullInt64{
    Int64: 传入值,
    Valid: true, // 表示这个传入值有效,即使它是 0
}

Valid: false 表示传入值无效,即写入默认 Null。

查询或者更新数据的时候也同样会遇到零值问题。

参考:

https://juejin.cn/post/7354940230301483017

本地化WordPress 的新浪微博图片外链

作者:matrix 发布时间:2024-09-30 分类:Golang 零零星星

😀 免费的才是最贵的。新浪微博的图床早就挂了,目前的图片会限制请求头 referer

今天空了才把这部分图片迁移到本地。记录下这个临时脚本。

脚本下载 WordPress 文章中的新浪图片到本地,然后数据库中的图片链接会执行替换。
配置好信息之后正式执行记得放开 #94行的TODO。 自行测试~

package main

import (
    "database/sql"
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "regexp"

    _ "github.com/go-sql-driver/mysql"
)

const (
    //TODO
    domain       = "www.hhtjim.com"                                             //WordPress 域名
    db_user      = "root"                                                       //db 信息
    db_password  = "root"                                                       //db 信息
    db_host_port = "127.0.0.1:33060"                                            //db 信息
    db_name      = "wordpress_db"                                               //db 信息
    savePathBase = "/home/www/htdoc/wordpress/wp-content/uploads/2024/sinaimg/" //图片下载保存位置
    newBase      = "https://" + domain + "/wp-content/uploads/2024/sinaimg/"    // 替换后的图片 url path
    referer      = "https://m.weibo.cn"
    dsn          = db_user + ":" + db_password + "@tcp(" + db_host_port + ")/" + db_name
    query        = "SELECT ID, post_content,post_content_filtered FROM wp_posts WHERE post_type = 'post' AND post_content LIKE '%sinaimg.cn%'  ORDER BY ID DESC;"
)

var (
    re = regexp.MustCompile(`//([a-zA-Z\d]+.sinaimg.cn)/([\w]+/[\w_-]+.(?:jpg|png|gif))`)
)

func main() {
    // 连接数据库
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Failed to connect to database:", err)
        return
    }
    defer db.Close()

    rows, err := db.Query(query)
    if err != nil {
        fmt.Println("Failed to query database:", err)
        return
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var postContent string
        var post_content_filtered string
        if err := rows.Scan(&id, &postContent, &post_content_filtered); err != nil {
            fmt.Println("Failed to scan row:", err)
            continue
        }

        updatedContent := re.ReplaceAllStringFunc(postContent, func(match string) string {
            matches := re.FindStringSubmatch(match)
            if len(matches) != 3 {
                return match
            }
            imgURL := "https:" + match
            savePath := matches[2]

            if _, err := os.Stat(savePath); os.IsNotExist(err) {
                if err := downloadImage(imgURL, savePath); err != nil {
                    fmt.Println("Failed to download image:", err)
                    return match
                }
            }
            return newBase + savePath
        })

        updatedContentFiltered := re.ReplaceAllStringFunc(post_content_filtered, func(match string) string {
            matches := re.FindStringSubmatch(match)
            if len(matches) != 3 {
                return match
            }
            imgURL := "https:" + match
            savePath := matches[2]

            if _, err := os.Stat(savePath); os.IsNotExist(err) {
                if err := downloadImage(imgURL, savePathBase+savePath); err != nil {
                    fmt.Println("Failed to download image:", err)
                    return match
                }
            }
            return newBase + savePath
        })

        fmt.Println(id, updatedContent, updatedContentFiltered)
        // fmt.Printf("https://%s?p=%d\n", domain, id)
        continue //TODO
        // break

        _, err := db.Exec("UPDATE wp_posts SET post_content = ?,post_content_filtered=? WHERE ID = ?", updatedContent, updatedContentFiltered, id)
        if err != nil {
            fmt.Println("Failed to update database:", err)
            continue
        }
    }
}

func downloadImage(imgURL, savePath string) error {
    resp, err := http.Get(imgURL)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("failed to download image: %s", resp.Status)
    }

    dir := filepath.Dir(savePath)
    if err := os.MkdirAll(dir, 0755); err != nil {
        return err
    }

    file, err := os.Create(savePath)
    if err != nil {
        return err
    }
    defer file.Close()

    _, err = io.Copy(file, resp.Body)
    return err
}

Golang动态可变函数参数 参数默认值

作者:matrix 发布时间:2024-08-17 分类:Golang

Golang是不支持函数参数默认值的,但是也有很多办法可以解决

动态可变参数

func main() {
    addItem("11", "a1")
    addItem("2", "a2", "222")
}

func addItem(name, value string, opts ...string) {
    fmt.Println("add item-->", name, value)
    for _, opt := range opts {
        fmt.Println("opt:", opt)
    }
}

其中opts ...string 表示可变参数,类型为string,如果需要不同类型传入 看下面

可变参数 + 动态类型

type AlfredItem struct {
    Title    string
    Subtitle string
    Arg      int
}

type AlfredWorkflow struct {
    Items []AlfredItem
}

func (aw *AlfredWorkflow) AddItem(name, value string, opts ...func(*AlfredItem)) {
    item := AlfredItem{
        Title:    value,
        Subtitle: name,
        Arg:      111,
    }
    for _, opt := range opts {
        opt(&item)
    }
    aw.Items = append(aw.Items, item)
}

func main() {
    aw := AlfredWorkflow{}
    aw.AddItem("A", "a")
    aw.AddItem("B", "b", func(ai *AlfredItem) {
        ai.Arg = 22222
    }, func(ai *AlfredItem) {
        ai.Arg = 3333
    })

    fmt.Printf("%+v", aw)
}

高阶用法 封装为选项模式(Option Pattern)

采用Functional Options Patter方法来解决

核心点:定义 type func(*AlfredItem),且每个参数定义with函数

type AlfredItem struct {
    Title    string
    Subtitle string
    Arg      int
}

type AlfredWorkflow struct {
    Items []AlfredItem
}

type Option func(*AlfredItem)

func WithTitle(title string) Option {
    return func(ai *AlfredItem) {
        ai.Title = title
    }
}
func WithSubtitle(subtitle string) Option {
    return func(ai *AlfredItem) {
        ai.Subtitle = subtitle
    }
}

func WithArg(arg int) Option {
    return func(ai *AlfredItem) {
        ai.Arg = arg
    }
}

func (aw *AlfredWorkflow) AddItem(name, value string, opts ...Option) {
    item := AlfredItem{
        Title:    value,
        Subtitle: name,
        Arg:      111,
    }
    for _, opt := range opts {
        opt(&item)
    }
    aw.Items = append(aw.Items, item)
}

func main() {
    aw := AlfredWorkflow{}
    aw.AddItem("DefaultName", "DefaultVlaue")
    aw.AddItem("DefaultName-B", "DefaultVlaue-b", WithArg(222), WithSubtitle("0000"))
    aw.AddItem("C", "c", WithTitle("hahah"))

    fmt.Printf("%+v", aw)
}

参考:

https://www.cnblogs.com/smartrui/p/10324320.html

go generate 为枚举类型生成字符串描述方法

作者:matrix 发布时间:2024-08-10 分类:Golang

go generate命令可以方便的为自动生成源代码,利用官方的stringer库来完成

安装stringer工具

如果本地已经安装,跳过

 go get -u golang.org/x/tools/cmd/stringer  

Case

main.GO

package main

import "fmt"

type UserStatus int
const (
    Active   UserStatus = 40
    Inactive UserStatus = 1
    Pending  UserStatus = 9
    Other               = Inactive
)

上面定义的常量类型UserStatus,原始类型为 int 值,每次使用 fmt.Print打印会只显示数字,可读性会很差。

那怎么让fmt.Print输出对应的描述?

自定义结构体String() 方法,打印时会自动调用

...
func (s UserStatus) String() string {
    switch s {
    case Active:
        return "Active"
    case Inactive:
        return "Inactive"
    case Pending:
        return "Pending"
    default:
        return "Other"
    }
}

func main(){
    var a UserStatus = Active
    fmt.Println(a) //Active
}

定义go:generate

上面手动编写的确可以,但如果有状态值调整后续维护会很麻烦,结合go:generate能自动生成String()方法

定义特定开头规则的注释//go:generate,这样go generate可以自动识别

//go:generate go run golang.org/x/tools/cmd/stringer -type=UserStatus
type UserStatus int

说明:
go:generate 表示go generate命令标记

go run golang.org/x/tools/cmd/stringer 表示stringer的执行命令,如果本地已经全局安装了其实也可以替换为stringer。但你得确保环境变量能够读取到它

-type 参数用于指定自定义的类型UserStatus

执行go:generate

go generate main.go 

不指定main.go 文件,generate命令会查找所有包含 //go:generate 指令的文件,并执行这些指令后面的命令。这个例子就会运行 stringer -type=UserStatus,为 UserStatus 类型生成一个新的 Go 文件userstatus_string.go,包含 String() 方法的实现。

自动生成的userstatus_string.go 文件

// Code generated by "stringer -type=UserStatus"; DO NOT EDIT.

package main

import "strconv"

func _() {
    // An "invalid array index" compiler error signifies that the constant values have changed.
    // Re-run the stringer command to generate them again.
    var x [1]struct{}
    _ = x[Active-40]
    _ = x[Inactive-1]
    _ = x[Pending-9]
}

const (
    _UserStatus_name_0 = "Inactive"
    _UserStatus_name_1 = "Pending"
    _UserStatus_name_2 = "Active"
)

func (i UserStatus) String() string {
    switch {
    case i == 1:
        return _UserStatus_name_0
    case i == 9:
        return _UserStatus_name_1
    case i == 40:
        return _UserStatus_name_2
    default:
        return "UserStatus(" + strconv.FormatInt(int64(i), 10) + ")"
    }
}

自动生成的代码中String()其实都大同小异,但是他考虑到了其他值。
并且_()匿名的函数内置逻辑用例可以起到防止枚举值被修改的问题,比如这里Active值被调整后会导致x[Active-40]取到非下标值导致编译失败 So Nice~
并且标注了DO NOT EDIT.

这样以后维护和构建过程更简单明了。

参考:

https://medium.com/@dadcod/6-unique-and-lesser-known-go-techniques-9821be24972b

https://www.jvt.me/posts/2022/06/15/go-tools-dependency-management/

stringer 源码:
https://cs.opensource.google/go/x/tools/+/master:cmd/stringer/stringer.go

Dockerfile多阶段构建镜像

作者:matrix 发布时间:2024-07-06 分类:Golang Linux

在构建GOdocker镜像时,都需要安装很多开发环境和依赖包,如果正常打包完整环境为镜像完全没有必要。因为运行时只需要Golang打包的二进制文件,不需要完整 dev 环境。

dockerfile多阶段构建就可以完美解决,将构建和运行环境分开,可以最终镜像最小化。 😆 😆 爽~~

# 第一阶段:使用开发环境镜像进行构建,设置别名builder
FROM golang:1.22 AS builder

# 设置工作目录
WORKDIR /app

# 复制所有文件到工作目录
COPY . .

# 编译应用程序
RUN go build -o go-demo .

# 第二阶段:使用小体积的基础镜像 打包最终镜像
FROM alpine:latest

WORKDIR /app

# 从构建阶段复制编译好的可执行文件
COPY --from=builder /app/go-demo .

# 运行可执行文件
CMD ["./go-demo"]

这样就可以确保最终的镜像只包含运行应用所需的最小文件,镜像环境也只是基础的alpine镜像。

golang远程调试 vscode+dlv

作者:matrix 发布时间:2023-12-31 分类:Golang

远程环境可能会有远程调试需求,比如白名单访问限制等情况

要让本地环境调试远程环境数据,本地代码和远程环境执行代码必须保持一致

安装dlv

远程服务器环境安装 dlv

$ go  install github.com/go-delve/delve/cmd/dlv@latest

查看已安装dlv版本

$ dlv version
Delve Debugger
Version: 1.21.0
Build: $Id: fec0d226b2c2cce1567d5f59169660cf61dc1efe 

启动dlv服务

方法a. 监听已启动进程

$ dlv attach 28122  --listen=:8669 --headless --api-version=2 --log

28122 为已启动进程id
8669 为dlv开启的监听端口

方法b. 通过dlv直接启动指定bin文件

$ dlv  exec   --listen=:8669 --headless --api-version=2 --log  ./main-hhtjim

8669 为dlv开启的监听端口
main-hhtjim 为打包的bin文件

方法c. dlv直接启动并且监听

$ dlv debug --listen=:8669 --headless --api-version=2 --log

8669 为dlv开启的监听端口

本地vscode启动debug

本地配置 .vscode/launch.json

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Remote",
      "type": "go",
      "request": "attach",
      "mode": "remote",
      "remotePath": "/home/work/demo-go/", //项目远程根路径
      "port": 8669, //监听端口
      "host": "www.hhtjim.com", //远程主机/IP
      "cwd": "${workspaceFolder}",//vscode本地工作目录
      "trace": "verbose" //输出详情
     }
  ]
}

销毁dlv监听

调试环境不使用之后切记销毁,dlv目前无身份校验!

GORM中使用虚拟字段

作者:matrix 发布时间:2023-11-30 分类:Golang

使用gorm时,可能需要处理虚拟字段(不在数据库中实际存在的字段)的情况。可以使用结构体tag标签来支持

User结构体模型

type User struct {
    ID    uint    `gorm:"primaryKey;not null"` // 主键ID

    // 虚拟字段
    Isvip int     `gorm:"-;default:0"`         // 是否vip 1是 0否
}

说明:

IsVip字段被标记为 gorm:"-" ,表示虚拟字段。GORM在进行数据库操作(如查询、插入、更新等)时,将不会考虑此字段。同时,可以使用default 标签为其指定默认值。

自定义获取器

自定义一个Get方法 例如,下面的GetIsVip方法会基于用户的VIP状态来返回相应的值:

func (u *User) GetIsVip() int {
    if u.Vip != nil && u.Vip.IsActive == 1 {
        return 1
    }
    return 0
}

应用获取器

在查询User对象时,GORM提供了 AfterFind 方法来自动执行特定逻辑。这在处理虚拟字段时很有用:

// 查询数据时自动赋值字段
func (u *User) AfterFind(tx *gorm.DB) (err error) {
    if u.Vip == nil {
        //TIPS:Association方法手动触发模型关联。如果使用Preload会再次查询User主表,不推荐
        // tx.Preload("Vip").First(&u, u.ID) //不推荐
        tx.Model(u).Association("Vip").Find(&u.Vip)
    }
    u.Isvip = u.GetIsVip() // 手动触发虚拟字段计算

    return
}

说明:

首先检查VIP信息是否已加载。如果未加载,则使用Association方法手动触发加载。之后,我们使用前面定义的GetIsVip方法来计算并设置Isvip字段的值。

注意

使用AfterFind可能会覆盖Isvip字段的默认值(如default:0

ShouldBindQuery获取GET参数默认值

作者:matrix 发布时间:2023-10-31 分类:Golang

图片5702-ShouldBindQuery读取GET参数默认值

验证器结构体的form标签中设置default即可,仅测试ShouldBindQuery有效读取,其他绑定方法未知

验证器结构体

type UserListValidator struct {
    Type     string `form:"type,default=RECOMMEND" binding:"omitempty,oneof=NEAR RECOMMEND" label:"列表类型"` //列表类型 NEAR:附近(默认) RECOMMEND:推荐
}

控制器方法

func (u *UserController) List(ctx *gin.Context) {
    validator := validators.UserListValidator{}
    if err := ctx.ShouldBindQuery(&validator); err != nil {
        u.JSONResponseError(ctx, err)
        return
    }
    //validator.Type 

参考:

https://github.com/gin-gonic/gin/issues/1052#issuecomment-1609678741

https://www.vksir.zone/posts/go_struct_default/