## Golang(go)+mongodb开发个人遇到坑及用法总结汇总
### 1为什么要选mongodb?
如果你是事务比较多的,就用mysql。
Mongodb存储的是json文档,不需要提前定义表结构。也支持事务。涉及关联查询的,可以选择json字段子文档(直接分别写一份或对象引用),这样就不用像mysql跨表关联查询,提高查询性能;涉及距离的查询\$geoNear;非商业版mongodb每个表只支持建一个text文本搜索索引(全文检索)(中文要自己分词后建立索引);
### 2Mongodb部署:
Windows开发阶段,装个微软Docker Desktop,部署mongodb镜像,redis等单机开发非常爽,方便。
Linux上线部署,建议最少3台(复制集,多台的可以副本集),采用PSS(绝对不要采用PSA仲裁节点,故障一个节点主节点写数据复制判断容易出问题。PSS可以使用rs.reconfig()把差机器外其他机器priority值设大优先选主)
https://www.mongodb.com/docs/manual/reference/configuration-options/\#core-options
配置
<https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-red-hat/>
vi /etc/yum.repos.d/mongodb-org-6.0.repo
sudo yum install mongodb-org -y
config={"_id":"mySet","members":[
{"_id":0,"host":"172.x.0.10:27017"},
{"_id":1,"host":"172.x.0.5:27017"},
{"_id":2,"host":"172.x.0.9:27017"}
]}
rs.initiate(config)
rs.status()
use admin
db.createUser(
{
user:"admin",
pwd:"xxx",
roles:[{role:"root",db:"admin"}]
}
);
db.auth("admin","xxx")
优先级越高的成员越有可能被选为主节点。默认情况下,副本集成员的选举优先级都是1,可以通过修改配置文件或者使用rs.reconfig()命令来设置
cfg = rs.conf()
cfg.members[0].priority = 10
cfg.members[1].priority = 9
cfg.members[2].priority = 1
rs.reconfig(cfg)
采用systemd自启动以及配置主动拉取,上面的rpm安装已经写了这个文件。
vim /usr/lib/systemd/system/mongod.service (可以修改指定自己的执行命令 –f 配置文件名)
sudo systemctl daemon-reload \#--修改后重载配置
设置 MongoDB 为系统服务,开机启动
sudo systemctl enable mongod
vim /etc/mongod.conf --配置文件
/usr/bin/mongod --服务
/usr/bin/mongo----客户端
### 3服务监控及自动拉取systemd:
比如我自己的go后台服务,配置成:
systemctl enable goSrv.service
vim /usr/lib/systemd/system/goSrv.service
sudo systemctl daemon-reload \#--修改后重载配置
[Unit]
Description=Gosrv
After=network.target
[Service]
User=xx
Group=xx
ExecStart=/opt/goSrv/cmd/start.sh
\#ExecReload=/bin/sh -c "/bin/kill -s USR1 \$(ps -ef \| grep goSrv \| grep -v 'grep' \| awk '{print \$2}'\| head -1)" \# shell 要这样写/bin/sh -c
ExecStop=/bin/sh -c "/bin/kill -s SIGTERM \$(ps -ef \| grep goSrv \| grep -v 'grep' \| awk '{print \$2}'\| head -1)"
Restart=on-failure \# 自动拉起
\#RestartSec=60s
StartLimitInterval=36000s \# 可以限制范围大一点,不然你修bug时手工拉取也算次数可能报错
StartLimitBurst=10
[Install]
WantedBy=multi-user.target
### 4 遇到的编译坑用法总结
#### 4.1. Gojieba windows本地开发正常,linux上编译执行,CGO堆栈异常。
gojieba内部调用了静态文件(各种utf8文件),而且不是通过embed方式调用; 编译时无法包含静态文件,运行时会报错。可以单独建个assets目录把gojieba_dict下所有文件放入,在代码中初始化NewJieba时传入目录位置。
```go
func InitGojieba() {
// 指定本地字典路径
exPath, err1 := utils.GetExPath("")
if err1 != nil {
global.G_LOG.Errorf("get path,err:%s", err1)
}
dictDir := filepath.Join(exPath, "assets/gojieba_dict")
global.G_LOG.Infof("gojeba,dictDir:%s", dictDir)
dpath := filepath.Join(dictDir, "jieba.dict.utf8")
hpath := filepath.Join(dictDir, "hmm_model.utf8")
upath := filepath.Join(dictDir, "user.dict.utf8")
ipath := filepath.Join(dictDir, "idf.utf8")
spath := filepath.Join(dictDir, "stop_words.utf8")
global.G_Jieba = gojieba.NewJieba(dpath, hpath, upath, ipath, spath)
}
```
#### 4.2 配置文件目录位置问题
编译后配置conf目录找不到了,这就是如何获取golang当前文件目录位置的问题。
```go
func GetFp() {
pathCurr, \_ := filepath.Abs(".")
fmt.Println(pathCurr)
// 不能用runtime.Caller(0) 它是写死编译时的堆栈获取的绝对路径(python \__file__可以,是因为它是解释执行,不是编译执行)
pathCurr2, \_ := os.Getwd()
fmt.Println(pathCurr2)
//
fmt.Println(os.Args[0])
//
exPath, err1 := os.Executable()
fmt.Printf("%s,err:%s\\n", exPath, err1)
}
```
主要4种方式,看看区别:
\#ide run执行
DAP server listening at: 127.0.0.1:56269
d:\\prj_sp\\v_test2\\cmd
d:\\prj_sp\\v_test2\\cmd
d:\\prj_sp\\v_test2\\cmd\\__debug_bin.exe
d:\\prj_sp\\v_test2\\cmd\\__debug_bin.exe
\#编译后目录执行
**PS D:\\prj_sp\> .\\run_test1\\filePathDis.exe**
**--**
D:\\prj_sp
D:\\prj_sp
D:\\prj_sp\\run_test1\\filePathDis.exe
D:\\prj_sp\\run_test1\\filePathDis.exe
**\# go run 执行 (还有直接单元测试)**
PS D:\\prj_sp\\v_test2\\cmd\> go run .\\main.go
\--
**D:\\prj_sp\\v_test2\\cmd**
**D:\\prj_sp\\v_test2\\cmd**
**C:\\Users\\zhao\\AppData\\Local\\Temp\\go-build2934688955\\b001\\exe\\main.exe**
**C:\\Users\\zhao\\AppData\\Local\\Temp\\go-build2934688955\\b001\\exe\\main.exe**
我自己写的golang获取当前目录方法,可以参考
```go
func GetExPath(traceId string) (exPathDir string, err error) {
\_, fl, line, ok := runtime.Caller(1)
if ok {
idx := strings.LastIndex(fl, "/")
if idx \> 0 {
fl = fl[idx+1:]
}
// fmt.Printf("[%s]caller %s to GetPathWd:%v\\n", traceId, fl, line)
}
// 不能用runtime.Caller(0) 它是写死编译时的堆栈获取的绝对路径
// pathCurr, err1 := os.Getwd() // 执行的目录
exPath, err1 := os.Executable()
if err1 != nil {
fmt.Printf("[%s]failed,caller %s\|%v 获取当前目录失败,err:%s\\n", traceId, fl, line, err1)
err = errors.New("获取当前目录失败")
} else {
// realPath, err1 := filepath.EvalSymlinks(exPath) // link
exPath = filepath.Dir(exPath)
exPathDir = filepath.Join(exPath, "../") // 移动main.go位置要修改这个文件,定位到工程主目录
//都定位到工程主目录
if IsWin() { // 开发时不是编译结果执行 go run或单元测试
//C:\\Users\\zhao\\AppData\\Local\\Temp\\go-build4236785995
if strings.Contains(exPath, "go-build") {
exPath, err1 = os.Getwd() //
if err1 != nil {
fmt.Printf("[%s]failed,caller %s\|%v 获取当前目录失败,err:%s\\n", traceId, fl, line, err1)
err = errors.New("获取当前目录失败")
} else { // 单元测试就是test的目录
if strings.Contains(exPath, "\\\\test\\\\") {
exPathDir = filepath.Join(exPath, "../../../")
}
}
}
}
//
exPathDir, err1 = filepath.Abs(exPathDir)
if err1 != nil {
fmt.Printf("[%s]failed,caller %s\|%v 获取当前目录失败,err:%s\\n", traceId, fl, line, err1)
err = errors.New("获取当前目录失败")
}
}
//
return
}
```
可以抽取一个config,一个assert目录,工程所有使用的读取文件都放这两个目录指定路径,开发及编译后使用都能正常。
#### 4.3 golang启动脚本增加异常打印堆栈
\#! /bin/bash
ulimit -c unlimited
export GOTRACEBACK=crash
/opt/goSrv/cmd/goSrv
### 5 go mongodb遇到的坑用法总结
#### 5.1. mongodb子文档查询(兼容字段不存在子文档情况) preserveNullAndEmptyArrays
```go
{
"\$unwind": bson.M{"path": "\$order_his", "preserveNullAndEmptyArrays": true},
},
非Group其他字段\$first
{
"\$group": bson.M{
"_id": "\$_id",
"brow_his": bson.M{"\$first": "\$brow_his"},
"order_his": bson.M{"\$push": "\$order_his"},
},
},
```
#### 5.2 mongodb filter子文档字段查询条件查不到结果
filter := bson.M{“fd1”:bson.M{“sfd1”:”v1”}}字段写成这样查询条件查不到结果,原因暂时没研究。要写成:
filter := bson.M{“fd1.sfd1”:”v1”}
#### 5.3 golang bson.M动态条件组合语句用法
var update bson.M = bson.M{"\$inc": bson.M{}, "\$set": bson.M{}}
注意这里的\$inc等关键字一定要先赋值,否则interface转的时候.(bson.M),会panic
if req.Img != "" {
update["\$set"].(bson.M)["img_mdy"] = req.Img
#### 5.4 mongodb事务
```go
s, err := r.C.Database().Client().StartSession() // 开启session
if err != nil {
global.G_LOG.Errorf("[%s] failed,修改前开启事务session错误,err:%s", r.TraceId, err)
err = errors.New("修改前开启事务session错误")
return
}
defer s.EndSession(context.TODO())
s.StartTransaction() // 开启事务
todo逻辑
if err != nil {
global.G_LOG.Errorf("[%s]设置%v失败,err:%s", r.TraceId, coinUse, err)
s.AbortTransaction(context.TODO())
return
}
//最后提交事务
s.CommitTransaction(context.TODO()) // 提交事务
return
```
#### 5.5 mongodb update判断设置成功
如果自己有些可能已经更新的可以不判断ModifiedCount
```go
resultU, err := r.C.UpdateOne(context.TODO(), filter, update)
if err == nil && resultU.MatchedCount != 1 && resultU.ModifiedCount == 0 {
global.G_LOG.Errorf("[%s] failed,Matched %v documents and updated %v documents.", r.TraceId, resultU.MatchedCount, resultU.ModifiedCount)
err = errors.New("查询及更新条数不正确")
}
```
#### 5.6 mongodb 动态删除数组
```go
delCntDo := int(float64(delCnt) \* 1.5)
strDelCntDo := strconv.Itoa(delCntDo)
// 删除大于条数,保留后面新的
//\$size needs a number
// filter = bson.M{"_id": objId, "brow_his": bson.M{"\$size": bson.M{"\$gte": delCntDo}}}
filter = bson.M{"_id": objId, "brow_his." + strDelCntDo: bson.M{"\$exists": true}}
// 超过一半一起删 //数组尾部加上新添加的数组each,然后进行切割 倒着保留
update = bson.M{"\$push": bson.M{"brow_his": bson.M{"\$each": bson.A{}, "\$slice": -delCnt}}}
```
#### 5.7 mongodb \$inc 小于0问题
如果golang结构体字段设置为uint等,查询后解析就会报错,查不出这条记录
```go
// update["\$inc"].(bson.M)["fans_cnt"] = -1
updateDec = bson.M{"\$inc": bson.M{"fans_cnt": -1}}
filterDec = bson.M{"_id": objId, "fans_cnt": bson.M{"\$gt": 0}}
我是单独更新这个字段,判断\$gt:0
if err == nil {
if updateDec["\$inc"] != nil { // 避免 -1
\_, err2 := r.C.UpdateOne(context.TODO(), filterDec, updateDec)
if err2 != nil {
global.G_LOG.Errorf("[%s]failed,update err:%s", r.TraceId, err2)
}
}
}
```
#### 5.8 mongodb SetArrayFilters 条件更新,及多个条件怎么写才对
```go
update["\$set"].(bson.M)["peers.\$[item].mark"] = mu.PeerInfo.Mark
updateOption.SetArrayFilters(options.ArrayFilters{
Filters: []interface{}{
bson.M{"item.uinfo.uid": mu.PeerInfo.Uid},
}})
多个条件一定要用\$and 及数组bson.A,否则查出来的可能不是你预期的
updateOption := options.Update()
updateOption.SetArrayFilters(options.ArrayFilters{
Filters: []interface{}{
bson.M{"\$and": bson.A{bson.M{"item.refund_no": ru.RefundNo},
bson.M{"item.refund_state": ru.RefundOldState}}},
}})
```
#### 5.9 mongodb 查询不到结果,db明明有记录,怎么分析
解析的golang结构体不对,也可能解析不到返回结果,可以用elem bson.M
去解析成map再去检查,是查询条件问题还是解析问题。
```go
for cur.Next(context.TODO()) {
var elem models.ApprNtf
// var elem bson.M
err1 := cur.Decode(&elem)
if err1 != nil {
global.G_LOG.Errorf("[%s]failed,查询err: %s", r.TraceId, err1)
err = errors.New("解析出错")
}
results = append(results, elem)
}
if err1 := cur.Err(); err1 != nil {
global.G_LOG.Errorf("[%s]failed,查询err: %s", r.TraceId, err1)
err = errors.New("cur出错")
}
if len(results) == 0 {
err = mongo.ErrNoDocuments
}
```
#### 5.10 mongodb \$in一定要注意转成bson.A,直接传slice或array查询结果不对
```go
"members.memb_state": bson.M{"\$in": bson.A{models.ResApprNotNeed, models.ResApprPass}},
rIds := bson.A{}
for \_, v := range urefRid {
objRid, \_ := w.ToMongoId(v)
rIds = append(rIds, objRid)
}
//
filter := bson.M{"_id": objId, "r_data": bson.M{"\$elemMatch": bson.M{"_id": bson.M{"\$in": rIds},
```
#### 5.11 golang匿名结构体写mongodb 怎么写tag
一定要写成这样bson:",inline",不能空着。mongodb存的时候才不是子文档,而是匿名结构体中的每个字段保存。
```go
type QryUserRefForMyRsp struct {
QryUserRefForMy \`bson:",inline"\`
PicMapVtime \*time.Time \`json:"pic_map_vtime" bson:"pic_map_vtime,omitempty"\`
}
```
#### 5.12 欢迎大家关注使用我golang+mongodb做的小程序(搜索 用享)