Go避坑指南

问题1:字符串序列化后,&符号 转成 “\u0026“ #

问题描述: #

从CGO拿到的字符串,序列化后存入数据库后,& < > 符号变成了类似 “\u0026"的形式,但编译器、界面等其他地方看到的确实原始的& < > 符号。

解决方案: #

数据结构中的值 带有 & > < 等符号,当我们要将 struct map 转成json时,使用

json.Marshal() 函数,此函数会将 值中的 & < > 符号转义 为 类似 “\u0026”

parm := make(map[string]string)
parm["path"] = "http://baidu.com?a=djflks&b=1231131"
 
//转成json 不转义特殊字符    这段替换原来的json.marshal  
bf := bytes.NewBuffer([]byte{})
jsonEncoder := json.NewEncoder(bf)
jsonEncoder.SetEscapeHTML(false)
jsonEncoder.Encode(parm)
fmt.Println(bf.String())

问题2:IDEA大面积报红,但编译能通过 #

问题描述: #

在使用IDEA编译代码的伙伴,偶合会发现个别函数报红,鼠标悬停显示函数未声明或者找不到等等,但是go build 却可以通过。

解决方案: #

IEDA缓存混乱的问题,点击 文件->修复IDE->

根据右下角提示,一路点到底,重新建立索引就好了。

问题4:CGO不支持在函数参数列表中使用默认参数 #

问题描述: #

使用CGO调用别人的动态库时,经常出现这种问题:

cgo: 
gcc errors for preamble:
In file included from .\hikvision.go:6:0:
C:/Users/c/go/src/VideoForensic/GoldenEyes/videogoplugin/networkscan/include/HCNetSDK.h:51623:68: error: expected ';', ',' or ')' before '=' token
 NET_DVR_API BOOL __stdcall NET_DVR_SetConnectTime(DWORD dwWaitTime = 3000, DWORD dwTryTimes = 3);
                                                                    ^
C:/Users/c/go/src/VideoForensic/GoldenEyes/videogoplugin/networkscan/include/HCNetSDK.h:51624:66: error: expected ';', ',' or ')' before '=' token
 NET_DVR_API BOOL __stdcall NET_DVR_SetReconnect(DWORD dwInterval = 30000, BOOL bEnableRecon = TRUE);

解决方案: #

找到引入的.h文件,找到相应函数定义,修改默认参数值。

NET_DVR_API BOOL __stdcall NET_DVR_SetReconnect(DWORD dwInterval = 30000, BOOL bEnableRecon = TRUE);
改成这样
NET_DVR_API BOOL __stdcall NET_DVR_SetReconnect(DWORD dwInterval, BOOL bEnableRecon);

问题5:CGO不识别自定义枚举类型 #

问题描述: #

使用CGO调用别人动态库时,类似如下情况:

error: unknown type name 'ADDITIONAL_LIB'; did you mean 'PARTITION_LDM'?
 NET_DVR_API BOOL __stdcall NET_DVR_LoadAdditionalLib(ADDITIONAL_LIB libType, char const *sDllName);
                                                      ^~~~~~~~~~~~~~
                                                      PARTITION_LDM

解决方案: #

CGO不认下面这个

enum ADDITIONAL_LIB 
{
    PLAYCTRL = 0,  
    DSSDK,        
    STREAMCONVERT,  
    STREAMTRANS,   
    QOSSDK,     
    DLL_PATH_AUDIO,  
    EZVIZ_SSL_SDK, 
    ANALYZE_DATA_LIB,
    DLL_LIBICONV,   
    SSLEAY32_SDK, 
    LIBEAY32_SDK,
    HCNETUTILS_SDK, 
    NPQ_LIB,  
    LOAD_DLL_COUNT,  
};

改成这样

typedef enum
{
    PLAYCTRL = 0,
    DSSDK,
    STREAMCONVERT,
    STREAMTRANS,
    QOSSDK,
    DLL_PATH_AUDIO,
    EZVIZ_SSL_SDK,
    ANALYZE_DATA_LIB,
    DLL_LIBICONV,
    SSLEAY32_SDK,
    LIBEAY32_SDK,
    HCNETUTILS_SDK,
    NPQ_LIB,
    LOAD_DLL_COUNT,
} ADDITIONAL_LIB;

问题6:CGO中不能使用引用(&)语法,这是 C++ 语言的特性 #

问题描述: #

使用CGO调用别人动态库时,编译出现类似以下错误:

C:/Users/c/go/src/VideoForensic/GoldenEyes/videogoplugin/networkscan/include/dhnetsdk.h:73691:148: error: expected ';', ',' or ')' before '&' token
 typedef void (CALLBACK *fSubLogDataCallBack)(LLONG lLogHandle, NET_EM_LOG_QUERY_TYPE emLogType, const DH_DEVICE_LOG_ITEM_EX *pstuLogData, const int& nCount, LDWORD dwUser, void *reserved);
                                                                                                                                                    ^
C:/Users/c/go/src/VideoForensic/GoldenEyes/videogoplugin/networkscan/include/dhnetsdk.h:76134:64: error: unknown type name 'fSubLogDataCallBack'; did you mean 'fLogDataCallBack'?
 CLIENT_NET_API void CALL_METHOD CLIENT_SetSubscribeLogCallBack(fSubLogDataCallBack pLogDataCB, LDWORD dwUser);
                                                                ^~~~~~~~~~~~~~~~~~~
                                                                fLogDataCallBack

解决方案: #

CGO中不能使用引用(&)语法,这是 C++ 语言的特性。

将 const int& nCount 改为 const int* nCount,并确保正确的类型名称定义:

typedef void (CALLBACK *fSubLogDataCallBack)(LLONG lLogHandle, NET_EM_LOG_QUERY_TYPE emLogType, const DH_DEVICE_LOG_ITEM_EX *pstuLogData, const int* nCount, LDWORD dwUser, void *reserved);

问题7:GORM中,Limit函数入参为0,查不到数据 #

问题描述: #

当我们使用gorm数据库框架查询数据时,若使用limit函数,函数入参没有赋值,则会默认为0,查不到任何数据。

eng := engine.Table(tableName).Where("pid=?", pid)
err = eng.Limit(limit).Offset(skip).Count(&nCount).Find(&resultData).Error

解决方案: #

若要查全部内容,limit设为-1

若有其他需求,limit自行赋值。

问题8:GORM中,使用更新仅适用于非零值 #

问题描述: #

当我们使用gorm数据库框架时,当使用struct更新时,FORM将仅更新具有非空值的字段

对于下面的更新,什么都不会更新为”",0,false是其类型的空白值

db.Model(&user).Updates(User{
   Name: "", Age: 0, Actived: false})

若想要更新空值,需要用到AllCols()函数

engine.Id(id).AllCols().Update(bean, condiBeans...)

问题9:GORM中,Count方法不适合放在raw方法后面,否则将会出错 #

问题描述: #

当我们使用gorm数据库框架时,Count方法放在raw方法后面,会出错

count:=0
db.Raw(sql).Count(&count)

问题10: crontab 定时脚本无法执行 #

问题描述: #

如果我们使用 crontab 来定时执行脚本,无法执行,但是如果直接通过命令(如:./test.sh)又可以正常执行,这主要是因为无法读取环境变量的原因。

解决方法: #

  • 1、所有命令需要写成绝对路径形式,如: /usr/local/bin/docker

  • 2、在 shell 脚本开头使用以下代码:

    #!/bin/sh
    
    . /etc/profile
    . ~/.bash_profile
    
  • 3、在 /etc/crontab 中添加环境变量,在可执行命令之前添加命令 . /etc/profile;/bin/sh,使得环境变量生效,例如:

    20 03 * * * . /etc/profile;/bin/sh /var/www/runoob/test.sh
    

使用crontab启动脚本,但找不到环境变量 pnpm go node

解决方法:

sudo ln -s /....   /usr/bin

问题11:Windows文件夹名,文件名不区分大小写 #

问题描述: #

使用Windows系统时,系统默认不区分文件夹和文件名的大小写。

func Test_path(t *testing.T) {
	path1 := "E:\\视频取证测试\\live.mp4\\"
	path2 := "E:\\视频取证测试\\live.MP4\\"
	if !IsDirExist(path1) {
		err := os.MkdirAll(path1, os.ModePerm)
		if err != nil {
			return
		}
	}
	if !IsDirExist(path2) {
		err := os.MkdirAll(path1, os.ModePerm)
		if err != nil {
			return
		}
	}
}
func IsDirExist(path string) bool {
	fi, err := os.Stat(path)
	if err != nil {
		// fmt.Println("check dir failed:", err.Error())
		return false
	}
	if fi.IsDir() {
		return true
	}
	return false
}

解决方法: #

统一将字符串转小写,进行过滤改名。

并发必坑总结 #

清理数据时,不要频繁创建新切片 #

每次提交数据后,代码都会重新创建一个新切片。可以预先分配一个较大的切片并维护其大小,从而减少内存分配和垃圾回收的开销。

				case <-tick: //五秒提交一次数据
				if len(fileData) > 0 {
					err := task.CommitData(file2.FileType, fileData)
					if err != nil {
						common.Log.Error(err.Error())
					}
					fileData = fileData[:0]   //重置就行
				}
				if len(recoveredData) > 0 {
					err := task.CommitData(file2.RecoveredFileType, recoveredData)
					if err != nil {
						common.Log.Error(err.Error())
					}
					recoveredData = recoveredData[:0]
				}
				if len(relateFiles) > 0 {
					err := task.CommitData(file2.RelateBlockType, relateFiles)
					if err != nil {
						common.Log.Error(err.Error())
					}
					relateFiles = relateFiles[:0]
				}
				if len(pictureData) > 0 {
					g.addPictureDataCount(int64(len(pictureData)))

					pictureTree.ChildCount = g.PictureToTalCount - int64(len(pictureData))
					pictureTree.ChildDeleteCount = g.DelPictureCount
					pictureTree.WriteData(pictureData)
					pictureData = make([]file2.PictureInfo, 0)
				}
				if len(videoData) > 0 {
					g.addVideoDataCount(int64(len(videoData)))

					tree.ChildCount = g.VideoTotalCount - int64(len(videoData))
					tree.ChildDeleteCount = g.DelVideoCount
					tree.WriteData(videoData)
					videoData = make([]file2.VideoTapeInfo, 0)
				}

使用 time.NewTicker 代替 time.Tick #

time.Tick 会创建一个无法停止的定时器,可能会导致资源泄露。推荐使用 time.NewTicker,这样可以在完成任务时停止定时器,避免内存泄漏。ticker的通道是单向通道,有利于代码接口的严谨性。

tick := time.Tick(time.Second * 3)
 ticker := time.NewTicker(3 * time.Second)
 defer ticker.Stop() // 确保定时器在任务结束时停止

for循环中带协程,要传参 #

var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
func main() {
    wg.Add(len(nums))
    for _, num := range nums {
        go func() {
            defer wg.Done()
            fmt.Println(num)
        }()
    }
    wg.Wait()
}

for循环中的goroutine在实际运行的时候,循环已经执行完毕了,num的值为循环后的最后一个值

var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
func main() {
    wg.Add(len(nums))
    for _, num := range nums {
        go func(num *int) {
            defer wg.Done()
            fmt.Println(*num)
        }(&num)
    }
    wg.Wait()
}

当你改成这样时,又掉入了下一个坑,go func()里保存了同一个内存地址,即&numfor循环中指向的是同一个内存地址,但该地址上存储的值在for中不断发生变化。

假如我们想将处理后的结果保存起来

var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var  results  []int
func main() {
    wg.Add(len(nums))
    for _, num := range nums {
        go func(num int) {
            defer wg.Done()
            results = append(results, num)
        }(num)
    }
    wg.Wait()
    fmt.Println(results)
}

如果你的结论是results中包含顺序不定的4 1 2 3 7 10 5 8 9 6,那么恭喜你又入坑了!正常情况下,len(results)的值应该为10,但上面代码多运行几次的结果表明,len(results)的值几乎都是小于10的,如果你拿到了正确值,建议多跑几次。因为在go中,切片slice类型是非并发安全的,也就是说results中的某一个位置在同一时刻插入了多个值,最终造成了数据丢失。

用 for range 来遍历数组或者 map 的时候,被遍历的指针是不变的,每次遍历仅执行 struct 值的拷贝 #

func main(){
    var stus []student

    stus = []student{
        {Name:"one", Age: 18},
        {Name:"two", Age: 19},
    }

    data := make(map[int]*student)

    for i, v := range stus{
        data[i] = &v   //应该改为:data[i] = &stus[i]
    }

    for i, v := range data{
        fmt.Printf("key=%d, value=%v \n", i,v)
    }
}
key=0, value=&{two 19} 
key=1, value=&{two 19}

Go 中没有继承!Go 中是叫组合! #

import "fmt"

type student struct{
    Name string
    Age  int
}

func (p *student) love(){
    fmt.Println("love")

}

func (p *student) like(){
    fmt.Println("like first")
    p.love()
}

type boy struct {
    student
}

func (b * boy) love(){
    fmt.Println("hate")
}

func main(){

    b := boy{}

    b.like()
}
like first
love

并不是使用 new 就一定会在堆上分配内存 #

编译器会自动选择在栈上还是在堆上分配存储空间,但可能令人惊讶的是,这个选择并不是由用 var 还是 new 声明变量的方式决定的。

var global *int
func f() {
    var x int x=1  
    global = &x 
} 
func g() {
   y := new(int)    
   *y = 1 
}

f()函数中的 x 就是在堆上分配内存,而 g()函数中的 y 就是分配在栈上。

init 函数在同一个文件中可以包含多个 #

在同一个包文件中,可以包含有多个 init 函数,多个 init 函数的执行顺序和定义顺序一致。

Golang 中没有“对象” #

type test struct {
    name string 
} 
func (t *test) getName(){
    fmt.Println("hello world")
}
func main() {
    var t *test
    t = nil
    t.getName()
}

能正常输出吗?会报错吗?

输出为:

hello world

可以正常输出。Go 本质上不是面向对象的语言,Go 中是不存在 object 的含义的,Go 语言书籍中的对象也和 Java、PHP 中的对象有区别,不是真正的”对象”,是 Go 中 struct 的实体。

调用 getName 方法,在 Go 中还可以转换,转换为:Type.method(t Type, arguments)

所以,以上代码 main 函数中还可以写成:

func main() {
   (*test).getName(nil)
}

map 引用不存在的 key,不报错 #

请问下面的例子输出什么,会报错吗?

func main(){
    newMap := make(map[string]int)
    fmt.Println(newMap["a"])
}

答案是:

不报错。不同于 PHP,Golang 的 map 和 Java 的 HashMap 类似,Java 引用不存在的会返回 null,而 Golang 会返回初始值