Go+MongoDB的微服务实战

介绍

本文章我们来学习一下使用Go对MongoDB数据库的实战,这里我们将以微信小程序中的三个微服务为背景,分别来实现对这三个微服务的CRUD实战,来体验Go和MongoDB在实际开发中的魅力

环境配置

开发环境:VScode

golang:go语言官方

MongoDB:使用driver@v1.8.4/mongo">MongoDB官方包

微信小程序介绍

该小程序是一款租车小程序dome,使用GRPC框架引领全栈开发,前端:typescrtp+wxml+wxss,后端:Go,GRPC框架,数据库:MongoDB

还在学习中

微服务的介绍与CRUD的实现

微服务就是把后端服务拆分为多个微小服务,来防止各服务之间的领域入侵,更能有效的开发和后期维护等

微服务一:用户登录实战(auth)

先来看看微信小程序登录的时序图:

这里的流程其实很清晰,我们看到小程序发送code至开发者服务器,接着开发者服务器需要调用相关的方法和api去携带appid, appsecret和code上传至微信相关服务去换取session_key和openid。

那么重点来了:这里我们不将openid直接与自定义登录态关联,而是需要将我们拿到的openid进行保存,拿出该openid在数据库中的索引(统一叫id),与自定义登录态关联,然后往下验证。

所以这里我们涉及的内容就是:

  1. 如何将openid存入数据库中
  2. 如何创建openid的索引id
  3. 如何拿出索引id
  4. 如何给定id拿出openid

下面我们来实现:

这需要先了解一下MongoDB的索引,即:”_id”, 该字段必须接受的是一个primitive.ObjectID类型的值,所以在使用是就需要涉及到类型转换了。

我们直接将所有的类型转换方法放入一个包中:objid

package objid

import (
    "coolcar/shared/id"
    "fmt"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

//ToAccount将 primitive.ObjectID转换为string id
func ToAccountID(oid primitive.ObjectID) id.AccountID {
    return id.AccountID(oid.Hex())
}

这里还有一个问题我们传入的是一个openid是string类型,我们拿出的_id也需要转换为string类型,返回出来,万一我们把openid和_id弄反了怎么办,大家都是string,编辑器也不会提示,所以这里需要做强化类型处理。

强类型化包:id

package id

//强类型化: AccountID定义account id对象类型
type AccountID string

func (a AccountID) String() string {
    return string(a)
}

这样我们拿出的_id转换成一个AccountID类型

接下来我们来回答这四个问题:

  1. 如何将openid存入数据库中
  2. 如何创建openid的索引id
  3. 如何拿出索引id
  4. 如何给定id拿出openid

方法声明:

func (*mongo.Collection).FindOneAndUpdate(ctx context.Context, filter interface{}, update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult

介绍:

FindOneAndUpdate 执行 findAndModify 命令以更新集合中的最多一个文档,并返回更新前的文档。

完整实现:见注释

package dao

import (
    "context"
    "coolcar/shared/id"
    "coolcar/shared/mongo/objid"
    "fmt"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
  "go.mongodb.org/mongo-driver/bson/primitive"
)

const (
    IDFieldName = "_id"  //存入数据库的字段名   
    openidfield = "open_id"//定义一个 Mongo 类型
type Mongo struct {
    col *mongo.Collection
}

//初始化数据库, 类似构造函数
func NewMongo(db *mongo.Database) *Mongo {
    return &Mongo{
        col: db.Collection("auth"),
    }
}

//将openID存入数据库,返回对应_id给用户
func (m *Mongo) ResolveAccountID(c context.Context, openID string) (id.AccountID, error) {
  //筛选器,以openID为筛选条件
    filter := bson.M{
        openidfield: openID,
    }
  //生成一个primitive.ObjectID类型作为文档索引
    var insertedID primitive.ObjectID
  //更新的数据
    updata := bson.M{
        "$setOnInsert": bson.M{
            IDFieldName: insertedID,
            openidfield: openID,
        },
    }
    //去查找openID,如果查到的openID则将对应_id返回出来,没有openID则插入我们固定的insertedID,然后将对应_id返回出来
    res := m.col.FindOneAndUpdate(c, filter, updata, options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After))
    //检测是否返回成功
    if err := res.Err(); err != nil {
        return "", fmt.Errorf("cannot findOneAndUpdate: %v", err)
    }

  //解码格式,我们在解码的时候必须确定数据结构
    var row struct{
        ID primitive.ObjectID `bson:"_id"`
    }
    //解码
    err := res.Decode(&row)
    if err != nil {
        return "", fmt.Errorf("cannot Decode result: %v", err)
    }
  //做类型转换,再返回
    return objid.ToAccountID(row.ID), nil
}

这样我们就完成了第一个登录服务的CRUD

接下来开始第二个服务的实战吧!

微服务二:行程服务(trip)

在微服务二中我们需要做四件事情

  1. 创建行程
  2. 获取单个行程
  3. 根据条件批量获取行程
  4. 更新行程

在该服务中,我们同样需要做类型转换, 强类型化:

类型转换:

package objid

import (
    "coolcar/shared/id"
    "fmt"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

//FromID将一个id转换为Object id
func FromID(id fmt.Stringer) (primitive.ObjectID, error) {
    return primitive.ObjectIDFromHex(id.String())
}

//MustFromID将一个id转换为Object id
func MustFromID(id fmt.Stringer) primitive.ObjectID {
    oid, err := FromID(id)
    if err != nil {
        panic(err)
    }
    return oid
}

//ToAccount将 primitive.ObjectID转换为string id
func ToAccountID(oid primitive.ObjectID) id.AccountID {
    return id.AccountID(oid.Hex())
}

//ToTripID将 primitive.ObjectID转换为string id
func ToTripID(oid primitive.ObjectID) id.TripID {
    return id.TripID(oid.Hex())
}

强类型化:

package id

//强类型化: AccountID定义account id对象类型
type AccountID string

func (a AccountID) String() string {
    return string(a)
}

//TripID 定义一个trip id
type TripID string

func (t TripID) String() string {
    return string(t)
}

//Identity定义一个用户身份
type IdentityID string

func (i IdentityID) String() string {
    return string(i)
}

//CarId定义一个车辆id
type CarId string

func (c CarId) String() string {
    return string(c)
}

这一节中我们将大量使用到MongoDB的知识,所以我们将一些可以代码作为公共代码(微服务三也会用到),这样我们就可以将CRUD中的变量,常量全都提出。

mgo:

package mgo

import (
    "coolcar/shared/mongo/objid"
    "fmt"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

const (
    IDFieldName        = "_id"
    UpdatedAtFieldName = "updatedat"
)

//ObjID defines the object field
type IDField struct {
    ID primitive.ObjectID `bson:"_id"`
}

//UpdatedAtField 定义一个时间筛选器
type UpdatedAtField struct {
    UpdatedAt int64 `bson:"updatedat"`
}

//NewObjectID 生成一个object id , NewObjID是一个函数
var NewObjID = primitive.NewObjectID

//NewObjIDWithValue 生成id 为下一个NewObjID,对id进一步包装,
func NewObjIDWithValue(id fmt.Stringer) {
    NewObjID = func() primitive.ObjectID {
        return objid.MustFromID(id)
    }
}

//Updateda 返回一个合适的值,你赋值给它
var UpdatedAt = func() int64 {
    return time.Now().UnixNano() //当前时间取纳秒
}

//Set return a $set updata document
func Set(V interface{}) bson.M {
    return bson.M{
        "$set": V,
    }
}

func SetInsert(V interface{}) bson.M {
    return bson.M{
        "$setOnInsert": V,
    }
}

注意:_id为每一个行程记录的索引,即行程ID,每一个用户也会有一个accountID,他们存放在一条文档中,但行程ID为数据库文档索引。

完成准备工作开始CRUD:

  1. 创建行程

    //字段
    const (
        tripField      = "trip"
        accountIDField = tripField + ".accountid"
        statusField    = tripField + ".status"
    
    )
    //定义数据存储结果
    type TripRecord struct {
        mgo.IDField        `bson:"inline"`
        mgo.UpdatedAtField `bson:"inline"` //时间戳
        Trip               *rentalpb.Trip  `bson:"trip"`
    }
    //创建行程, 将初始化数据放入数据库中并分配Trip ID和时间戳
    func (m *Mongo) CreateTrip(c context.Context, trip *rentalpb.Trip) (*TripRecord, error) {
        r := &TripRecord{
            Trip: trip,
        }
        r.ID = mgo.NewObjID()
        r.UpdatedAt = mgo.UpdatedAt()
        _, err := m.col.InsertOne(c, r)
        if err != nil {
            return nil, err
        }
        return r, nil
    }
  2. 获取当个行程

    //根据条件获取行程信息
    func (m *Mongo) GetTrip(c context.Context, id id.TripID, accountId id.AccountID) (*TripRecord, error) {
        //将id做类型转换
        ojbid, err := objid.FromID(id)
        if err != nil {
            return nil, fmt.Errorf("不能将id转换: %v", err)
        }
    
      //注释为另一种写法
        // filter := bson.M{
        //     "id": ojbid,
        //     "trip.accountid": accountId,
        // }
        // res := m.col.FindOne(c, filter)
    
      //需要根据tripID和accountID进行筛选
        res := m.col.FindOne(c, bson.M{
            mgo.IDFieldName: ojbid,
            accountIDField:  accountId,
        })
    
        //将res以TripRecord的结构解码
        var tr TripRecord
        err = res.Decode(&tr)
        if err != nil {
            fmt.Errorf("不能解码: %v", err)
        }
        return &tr, nil
    }
  3. 根据条件批量获取行程

    这里我们还需要根据行程状态(未开始, 进行中, 已完成)进行获取

    //GetTrips 根据条件去批量获取用户的行程信息
    func (m *Mongo) GetTrips(c context.Context, accountID id.AccountID, status rentalpb.TripStatus) ([]*TripRecord, error) {
        filter := bson.M{
            accountIDField: accountID.String(),
        }
        if status != rentalpb.TripStatus_TS_NOT_SPECIFIED {
            filter[statusField] = status
        }
    
        res, err := m.col.Find(c, filter, options.Find().SetSort(bson.M{
            mgo.IDFieldName: -1,
        }))
    
        if err != nil {
            return nil, fmt.Errorf("cannot Find matching documents: %v", err)
        }
    
        var trips []*TripRecord
        for res.Next(c) {
    
            //将res以TripRecord的结构解码
            var trip TripRecord
            err := res.Decode(&trip)
            if err != nil {
                fmt.Errorf("不能解码: %v", err)
            }
            trips = append(trips, &trip)
        }
        return trips, nil
    }
  4. 更新行程

    
    //UpdateTrip 根据输入更新数据
    func (m *Mongo) UpdateTrip(c context.Context, tripid id.TripID, accountid id.AccountID, updatedAt int64, trip *rentalpb.Trip) error {
        ojbid, err := objid.FromID(tripid)
        if err != nil {
            //fmt.Errorf("类型转换失败: %v", err)
            return err
        }
        newUpdateAt := mgo.UpdatedAt()
        //筛选器,根据行程ID,用户ID和行程的时间戳进行筛选
        filter := bson.M{
            mgo.IDFieldName:        ojbid,
            accountIDField:         accountid.String(),
            mgo.UpdatedAtFieldName: updatedAt,
        }
        //更新数据
        change := mgo.Set(bson.M{
            tripField:              trip,
            mgo.UpdatedAtFieldName: newUpdateAt,
        })
    
        res, err := m.col.UpdateOne(c, filter, change)
        if err != nil {
            return err
        }
        if res.MatchedCount == 0 {
            return mongo.ErrNoDocuments
        }
    
        return nil
    }
    

    这就是整个微服务二的CRUD实战内容,下面是完整代码:

    package dao
    
    import (
        "context"
        rentalpb "coolcar/rental/api/gen/v1"
        "coolcar/shared/id"
        mgo "coolcar/shared/mongo"
        objid "coolcar/shared/mongo/objid"
        "fmt"
    
        "go.mongodb.org/mongo-driver/bson"
        "go.mongodb.org/mongo-driver/mongo"
        "go.mongodb.org/mongo-driver/mongo/options"
    )
    
    const (
        tripField      = "trip"
        accountIDField = tripField + ".accountid"
        statusField    = tripField + ".status"
    )
    
    //定义一个 Mongo 类型
    type Mongo struct {
        col *mongo.Collection
    }
    
    //初始化数据库, 类似构造函数
    func NewMongo(db *mongo.Database) *Mongo {
        return &Mongo{
            col: db.Collection("trip"),
        }
    }
    
    type TripRecord struct {
        mgo.IDField        `bson:"inline"`
        mgo.UpdatedAtField `bson:"inline"` //时间戳
        Trip               *rentalpb.Trip  `bson:"trip"`
    }
    
    //创建行程, 将初始化数据放入数据库中并分配Trip ID和时间戳
    func (m *Mongo) CreateTrip(c context.Context, trip *rentalpb.Trip) (*TripRecord, error) {
        r := &TripRecord{
            Trip: trip,
        }
        r.ID = mgo.NewObjID()
        r.UpdatedAt = mgo.UpdatedAt()
        _, err := m.col.InsertOne(c, r)
        if err != nil {
            return nil, err
        }
        return r, nil
    }
    
    //根据条件获取行程信息
    func (m *Mongo) GetTrip(c context.Context, id id.TripID, accountId id.AccountID) (*TripRecord, error) {
        //将id做类型转换
        ojbid, err := objid.FromID(id)
        if err != nil {
            return nil, fmt.Errorf("不能将id转换: %v", err)
        }
        // filter := bson.M{
        //     "id": ojbid,
        //     "trip.accountid": accountId,
        // }
        // res := m.col.FindOne(c, filter)
    
        res := m.col.FindOne(c, bson.M{
            mgo.IDFieldName: ojbid,
            accountIDField:  accountId,
        })
    
        //将res以TripRecord的结构解码
        var tr TripRecord
        err = res.Decode(&tr)
        if err != nil {
            fmt.Errorf("不能解码: %v", err)
        }
        return &tr, nil
    }
    
    //GetTrips 根据条件去批量获取用户的行程信息
    func (m *Mongo) GetTrips(c context.Context, accountID id.AccountID, status rentalpb.TripStatus) ([]*TripRecord, error) {
        filter := bson.M{
            accountIDField: accountID.String(),
        }
        if status != rentalpb.TripStatus_TS_NOT_SPECIFIED {
            filter[statusField] = status
        }
    
        res, err := m.col.Find(c, filter, options.Find().SetSort(bson.M{
            mgo.IDFieldName: -1,
        }))
    
        if err != nil {
            return nil, fmt.Errorf("cannot Find matching documents: %v", err)
        }
    
        var trips []*TripRecord
        for res.Next(c) {
    
            //将res以TripRecord的结构解码
            var trip TripRecord
            err := res.Decode(&trip)
            if err != nil {
                fmt.Errorf("不能解码: %v", err)
            }
            trips = append(trips, &trip)
        }
        return trips, nil
    }
    
    //UpdateTrip 根据输入更新数据
    func (m *Mongo) UpdateTrip(c context.Context, tripid id.TripID, accountid id.AccountID, updatedAt int64, trip *rentalpb.Trip) error {
        ojbid, err := objid.FromID(tripid)
        if err != nil {
            //fmt.Errorf("类型转换失败: %v", err)
            return err
        }
        //筛选器
        newUpdateAt := mgo.UpdatedAt()
    
        filter := bson.M{
            mgo.IDFieldName:        ojbid,
            accountIDField:         accountid.String(),
            mgo.UpdatedAtFieldName: updatedAt,
        }
        //更改数据
        change := mgo.Set(bson.M{
            tripField:              trip,
            mgo.UpdatedAtFieldName: newUpdateAt,
        })
    
        res, err := m.col.UpdateOne(c, filter, change)
        if err != nil {
            return err
        }
        if res.MatchedCount == 0 {
            return mongo.ErrNoDocuments
        }
    
        return nil
    }
微服务三:身份信息的验证(profile)

profile服务应该是和trip服务是一个微服务的,因为这里需要涉及到blob微服务,所以我将profile单独介绍,该服务的作用是将用户上传的身份信息进程存储和获取,以及和另一个blob微服务进行交互

这里我们先来实现用户身份存储CRUD:

通样我们需要类型转换:

package objid

import (
    "coolcar/shared/id"
    "fmt"

    //"strings"

    "go.mongodb.org/mongo-driver/bson/primitive"
)

//FromID将一个id转换为Object id
func FromID(id fmt.Stringer) (primitive.ObjectID, error) {
    return primitive.ObjectIDFromHex(id.String())
}

//MustFromID将一个id转换为Object id
func MustFromID(id fmt.Stringer) primitive.ObjectID {
    oid, err := FromID(id)
    if err != nil {
        panic(err)
    }
    return oid
}

//ToAccount将 primitive.ObjectID转换为string id
func ToAccountID(oid primitive.ObjectID) id.AccountID {
    return id.AccountID(oid.Hex())
}

//ToTripID将 primitive.ObjectID转换为string id
func ToTripID(oid primitive.ObjectID) id.TripID {
    return id.TripID(oid.Hex())
}

强类型化:

package id

//强类型化: AccountID定义account id对象类型
type AccountID string

func (a AccountID) String() string {
    return string(a)
}

//TripID 定义一个trip id
type TripID string

func (t TripID) String() string {
    return string(t)
}

//Identity定义一个用户身份
type IdentityID string

func (i IdentityID) String() string {
    return string(i)
}

//CarId定义一个车辆id
type CarId string

func (c CarId) String() string {
    return string(c)
}

//BlobID定义一个blobID
type BlobID string

func (b BlobID) String() string {
    return string(b)
}

公共代码:

package mgo

import (
    "coolcar/shared/mongo/objid"
    "fmt"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

const (
    IDFieldName        = "_id"
    UpdatedAtFieldName = "updatedat"
)

//ObjID defines the object field
type IDField struct {
    ID primitive.ObjectID `bson:"_id"`
}

//UpdatedAtField 定义一个时间筛选器
type UpdatedAtField struct {
    UpdatedAt int64 `bson:"updatedat"`
}

//NewObjectID 生成一个object id , NewObjID是一个函数
var NewObjID = primitive.NewObjectID

//NewObjIDWithValue 生成id 为下一个NewObjID,对id进一步包装,
func NewObjIDWithValue(id fmt.Stringer) {
    NewObjID = func() primitive.ObjectID {
        return objid.MustFromID(id)
    }
}

//Updateda 返回一个合适的值,你赋值给它
var UpdatedAt = func() int64 {
    return time.Now().UnixNano() //当前时间取纳秒
}

//Set return a $set updata document
func Set(V interface{}) bson.M {
    return bson.M{
        "$set": V,
    }
}

func SetInsert(V interface{}) bson.M {
    return bson.M{
        "$setOnInsert": V,
    }
}

//ZeroOrDoesNotExist是一个生成筛选器的表达式去筛选zero或者不存在的值
func ZeroOrDoesNotExist(field string, zero interface{}) bson.M {
    return bson.M{
        "$or": []bson.M{
            {
                field: zero,
            },
            {
                field: bson.M{
                    "$exists": false,
                },
            },
        },
    }
}

两方法个请求:

  1. 获取身份信息
  2. 更新身份信息
  3. 更新个人资料照片和blob id
package dao

import (
    "context"
    rentalpb "coolcar/rental/api/gen/v1"
    "coolcar/shared/id"
    mgo "coolcar/shared/mongo"
    "fmt"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

const (
    accountIDField      = "accountid"
    profileField        = "profile"
    identityStatusField = profileField + ".identitystatus"
    photoblobIDField    = "photoblobid"
)

type Mongo struct {
    col *mongo.Collection
}

//构造函数
func NewMongo(db *mongo.Database) *Mongo {
    return &Mongo{
        col: db.Collection("profile"),
    }
}

//ProfileRecord定义profile在数据库中的解码方式
type ProfileRecord struct {
    AccountID   string            `bson:"accountid"`
    Profile     *rentalpb.Profile `bson:"profile"`
    PhotoBlobID string            `bson:"photoblobid"`
}

//获取身份信息
func (m *Mongo) GetProfile(c context.Context, aid id.AccountID) (*ProfileRecord, error) {
    filter := bson.M{
        accountIDField: aid.String(),
    }
    res := m.col.FindOne(c, filter)
    //如果文档为空
    if err := res.Err(); err != nil {
        return nil, err
    }
    //对res进行解码
    var pr ProfileRecord
    err := res.Decode(&pr)
    if err != nil {
        return nil, fmt.Errorf("解码失败: %v", err)
    }
    return &pr, nil
}

//更新身份信息
func (m *Mongo) UpdateProfile(c context.Context, aid id.AccountID, prevState rentalpb.IdentityStatus, p *rentalpb.Profile) error {
    filter := bson.M{
        identityStatusField: prevState,
    }
    if prevState == rentalpb.IdentityStatus_UNSUBMITTED {
        filter = mgo.ZeroOrDoesNotExist(identityStatusField, prevState)

    }

    filter[accountIDField] = aid.String()

    change := mgo.Set(bson.M{
        accountIDField: aid.String(),
        profileField:   p,
    })
    _, err := m.col.UpdateOne(c, filter, change,
        options.Update().SetUpsert(true))
    if err != nil {
        return fmt.Errorf("更新失败:%v", err)
    }
    return nil
}

//UpdateProfilePhoto 更新个人资料照片和blob id。
func (m *Mongo) UpdateProfilePhoto(c context.Context, aid id.AccountID, bid id.BlobID) error {
    filter := bson.M{
        accountIDField: aid.String(),
    }
    change := mgo.Set(bson.M{
        accountIDField:   aid.String(),
        photoblobIDField: bid.String(),
    })
    _, err := m.col.UpdateOne(c, filter, change,
        options.Update().SetUpsert(true))
    if err != nil {
        return fmt.Errorf("更新失败:%v", err)
    }
    return err
}

微服务blob:工作流程图:

blob根据profile提供的accountID,并根据blob数据库该文档的索引和提供的accountID生成path,然后将一起存入数据库,再返回对应数据, 图片上传完成后,profile向blob提供blobID(即:blbo数据库文档索引),就可以拿到path,去云端获取图片了,下面来实现blob的CRUD:

同样类型转换,强化类型和mgo和profile中一致

CRUD:

  1. 创建一个blod记录
  2. 根据blobID或者信息
package dao

import (
    "context"
    "coolcar/shared/id"
    mgo "coolcar/shared/mongo"
    "coolcar/shared/mongo/objid"
    "fmt"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
)

type Mongo struct {
    col *mongo.Collection
}

//构造函数
func NewMongo(db *mongo.Database) *Mongo {
    return &Mongo{
        col: db.Collection("blob"),
    }
}

type BlobRecord struct {
    mgo.IDField `bson:"inline"`
    AccountID   string `bson:"accountid"`
    Path        string `bson:"path"`
}

//CreateBlob创建一个blod记录
func (m *Mongo) CreateBlob(c context.Context, aid id.AccountID) (*BlobRecord, error) {
    br := &BlobRecord{
        AccountID: aid.String(),
    }
    objID := mgo.NewObjID()
    br.ID = objID
    br.Path = fmt.Sprintf("%s/%s", aid, objID.Hex())
    fmt.Printf("MYRUL:%s\n", br.Path)

    _, err := m.col.InsertOne(c, br)
    if err != nil {
        return nil, err
    }
    return br, nil
}

//根据blobID或者信息
func (m *Mongo) GetBlob(c context.Context, bid id.BlobID) (*BlobRecord, error) {
    objID, err := objid.FromID(bid)
    if err != nil {
        return nil, fmt.Errorf("失效的objid id: %v", err)
    }
    filter := bson.M{
        mgo.IDFieldName: objID,
    }
    res := m.col.FindOne(c, filter)

    if err = res.Err(); err != nil {
        return nil, err
    }
    var br BlobRecord
    err = res.Decode(&br)
    if err != nil {
        return nil, fmt.Errorf("解码失败: %v", err)
    }
    return &br, nil
}

这里我们的几个微服务都做完了

总结

这就是Go和MongoDB在实战中是应用, 其实也很简单,我们需要注意数据库的索引_id的使用,以及各字段的写入,解码结构,和数据类型的强类型化,虽然看上去增加了代码的量,但是这保证了我们数据不会出问题。

本作品采用《CC 协议》,转载必须注明作者和本文链接
刻意学习
讨论数量: 2

好家伙,慕课网的视频,我正在学习的体系课的一部分

1年前 评论
ice_moss (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
118
粉丝
89
喜欢
173
收藏
246
排名:365
访问:2.6 万
私信
所有博文
社区赞助商