The efficient way to convert struct to bson and bson to struct - mongodb

What is the correct way to convert a struct to bson or bson to struct from the point of view of server performance?
bsonAsByte, err = bson.Marshal(&bsonData)
if err != nil {
panic(err)
}
err = bson.Unmarshal(bsonAsByte, &user)
if err != nil {
panic(err)
}

Most responses always use bson.Marshal() followed by bson.Unmarshal(), but this form is slower.
Imagine that you are on a server with thousands of accesses to the database per second. Your code can slow down server operation depending on the quality of the code.
For this reason, I always do the benchmark tests before choosing which method to use.
An example is shown below:
package bsontest
import (
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"reflect"
"testing"
)
type User struct {
UniqueID string `bson:"_id"`
Name string `bson:"name"`
Password string `bson:"password"`
Email string `bson:"email"`
Age int `bson:"age"`
Admin bool `bson:"admin"`
}
func (e *User) FromBson(data bson.M) (err error) {
var tagValue string
element := reflect.ValueOf(e).Elem()
for i := 0; i < element.NumField(); i += 1 {
typeField := element.Type().Field(i)
tag := typeField.Tag
tagValue = tag.Get("bson")
if tagValue == "-" {
continue
}
switch element.Field(i).Kind() {
case reflect.String:
switch data[tagValue].(type) {
case string:
element.Field(i).SetString(data[tagValue].(string))
default:
err = errors.New(tagValue+" must be a string")
return
}
case reflect.Bool:
switch data[tagValue].(type) {
case bool:
element.Field(i).SetBool(data[tagValue].(bool))
default:
err = errors.New(tagValue+" must be a boolean")
return
}
case reflect.Int:
switch data[tagValue].(type) {
case int:
element.Field(i).SetInt(int64(data[tagValue].(int)))
case int64:
element.Field(i).SetInt(data[tagValue].(int64))
default:
err = errors.New(tagValue+" must be a integer")
return
}
}
}
return
}
func (e *User) ToBson() (data bson.M) {
var tagValue string
data = bson.M{}
element := reflect.ValueOf(e).Elem()
for i := 0; i < element.NumField(); i += 1 {
typeField := element.Type().Field(i)
tag := typeField.Tag
tagValue = tag.Get("bson")
if tagValue == "-" {
continue
}
switch element.Field(i).Kind() {
case reflect.String:
value := element.Field(i).String()
data[tagValue] = value
case reflect.Bool:
value := element.Field(i).Bool()
data[tagValue] = value
case reflect.Int:
value := element.Field(i).Int()
data[tagValue] = value
}
}
return
}
var user User
func init() {
user = User{
UniqueID: "12345-67890-ABCDE-FGHIJKL",
Name: "Fulano da Silva Sauro",
Password: "pangea",
Email: "sauro#pangea.com",
Admin: true,
}
}
func ExampleUser_ToBson() {
var err error
var userAsBSon = user.ToBson()
fmt.Printf("%+v\n", userAsBSon)
user = User{}
fmt.Printf("%+v\n", user)
err = user.FromBson(userAsBSon)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", user)
// Output:
// map[_id:12345-67890-ABCDE-FGHIJKL admin:true age:0 email:sauro#pangea.com name:Fulano da Silva Sauro password:pangea]
// {UniqueID: Name: Password: Email: Age:0 Admin:false}
// {UniqueID:12345-67890-ABCDE-FGHIJKL Name:Fulano da Silva Sauro Password:pangea Email:sauro#pangea.com Age:0 Admin:true}
}
func Benchmark_UsingMarshalAndUnmarshal(b *testing.B) {
var err error
var bsonAsByte []byte
var bsonData bson.M
for i := 0; i < b.N; i++ {
bsonAsByte, err = bson.Marshal(&user)
if err != nil {
panic(err)
}
err = bson.Unmarshal(bsonAsByte, &bsonData)
if err != nil {
panic(err)
}
}
}
func Benchmark_UsingUnmarshalAndMarshal(b *testing.B) {
var err error
var bsonAsByte []byte
var bsonData = bson.M{
"_id":"12345-67890-ABCDE-FGHIJKL",
"admin":true,
"age":0,
"email":"sauro#pangea.com",
"name":"Fulano da Silva Sauro",
"password":"pangea",
}
for i := 0; i < b.N; i++ {
bsonAsByte, err = bson.Marshal(&bsonData)
if err != nil {
panic(err)
}
err = bson.Unmarshal(bsonAsByte, &user)
if err != nil {
panic(err)
}
}
}
func Benchmark_UsingFromBson(b *testing.B) {
var err error
var bsonData = bson.M{
"_id":"12345-67890-ABCDE-FGHIJKL",
"admin":true,
"age":0,
"email":"sauro#pangea.com",
"name":"Fulano da Silva Sauro",
"password":"pangea",
}
for i := 0; i < b.N; i++ {
err = user.FromBson(bsonData)
if err != nil {
panic(err)
}
}
}
func Benchmark_UsingToBson(b *testing.B) {
var bsonData bson.M
for i := 0; i < b.N; i++ {
bsonData = user.ToBson()
}
_ = bsonData
}
Timers:
Benchmark_UsingMarshalAndUnmarshal
Benchmark_UsingMarshalAndUnmarshal-8 398800 2996 ns/op
Benchmark_UsingToBson
Benchmark_UsingToBson-8 1789176 643.0 ns/op
Benchmark_UsingFromBson
Benchmark_UsingFromBson-8 2128242 539.6 ns/op
Benchmark_UsingUnmarshalAndMarshal
Benchmark_UsingUnmarshalAndMarshal-8 474501 2524 ns/op

Related

How to Handle Dynamic Database Connections in Go?

I am currently building a Go application that needs to connect to multiple databases dynamically.
For context I have 22 Databases (db1, db2, db3...) and the dbUser, dbPass and dbPort remains the same. To determine which database to connect to, I need access to the query param in echo before database connection.
I need a solution to connect to the right database efficiently. What are some best practices and methods for achieving this in Go?
Main.go
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
"github.com/labstack/echo"
"github.com/spf13/viper"
_variantHttpDelivery "backend/server/variant/delivery/http"
_variantHttpDeliveryMiddleware "backend/server/variant/delivery/http/middleware"
_variantRepo "backend/server/variant/repository/postgres"
_variantUcase "backend/server/variant/usecase"
)
func init() {
viper.SetConfigFile(`config.json`)
err := viper.ReadInConfig()
if err != nil {
panic(err)
}
if viper.GetBool(`debug`) {
log.Println("Service RUN on DEBUG mode")
}
}
func main() {
dbHost := viper.GetString(`database.host`)
dbPort := viper.GetString(`database.port`)
dbUser := viper.GetString(`database.user`)
dbPass := viper.GetString(`database.pass`)
dbName := viper.GetString(`database.name`)
connection := fmt.Sprintf("postgresql://%s:%s#%s:%s/%s", dbUser, dbPass, dbHost, dbPort, dbName)
dsn := fmt.Sprintf("%s?%s", connection)
dbConn, err := sql.Open(`postgres`, dsn)
log.Println("Connection Successful 👍")
if err != nil {
log.Fatal(err)
}
err = dbConn.Ping()
if err != nil {
log.Fatal(err)
}
defer func() {
err := dbConn.Close()
if err != nil {
log.Fatal(err)
}
}()
e := echo.New()
middL := _variantHttpDeliveryMiddleware.InitMiddleware()
e.Use(middL.CORS)
variantRepo := _variantRepo.NewPsqlVariantRepository(dbConn)
timeoutContext := time.Duration(viper.GetInt("context.timeout")) * time.Second
au := _variantUcase.NewVariantUsecase(variantRepo, timeoutContext)
_variantHttpDelivery.NewVariantHandler(e, au)
log.Fatal(e.Start(viper.GetString("server.address"))) //nolint
}
Repository which handles all the database logic
package postgres
import (
"backend/server/domain"
"context"
"database/sql"
"github.com/sirupsen/logrus"
"reflect"
)
type psqlVariantRepository struct {
Conn *sql.DB
}
// NewPsqlVariantRepository will create an object that represent the variant.Repository interface
func NewPsqlVariantRepository(conn *sql.DB) domain.VariantRepository {
return &psqlVariantRepository{conn}
}
func (m *psqlVariantRepository) GetByVCF(ctx context.Context, vcf string) (res domain.Variant, err error) {
query := `SELECT * FROM main1 WHERE variant_vcf = $1`
list, err := m.fetch(ctx, query, vcf)
if err != nil {
return domain.Variant{}, err
}
if len(list) > 0 {
res = list[0]
} else {
return res, domain.ErrNotFound
}
return
}
func (m *psqlVariantRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Variant, err error) {
rows, err := m.Conn.QueryContext(ctx, query, args...)
if err != nil {
logrus.Error(err)
return nil, err
}
defer func() {
errRow := rows.Close()
if errRow != nil {
logrus.Error(errRow)
}
}()
result = make([]domain.Variant, 0)
for rows.Next() {
t := domain.Variant{}
values := make([]interface{}, 0, reflect.TypeOf(t).NumField())
v := reflect.ValueOf(&t).Elem()
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).Type.Kind() == reflect.String {
values = append(values, new(sql.NullString))
} else {
values = append(values, v.Field(i).Addr().Interface())
}
}
err = rows.Scan(values...)
if err != nil {
logrus.Error(err)
return nil, err
}
for i, value := range values {
if ns, ok := value.(*sql.NullString); ok {
v.Field(i).SetString(ns.String)
}
}
result = append(result, t)
}
logrus.Info("Successfully fetched results from database 👍")
return result, nil
}
So far I couldn't find any solution

Mongo db driver doesn't update record

I'm creating an app using microservice architecture with go and mongodb. Now I'm trying to make a crud with users, but I faced with a small problem. When I try to update the record nothing changes. I don't know what can be the problem, cuz I took it from official documentation.
So here's my code:
user_service.go
type UserService struct {
protos.UserServiceServer
}
func (s *UserService) Update(_ context.Context, request *protos.UpdateRequest) (*protos.GetResponse, error) {
resultUser, err := user.Update(request)
if err != nil {
return response.CreateErrorGetResponse("couldn't update user"), nil
}
return response.CreateGetResponse(resultUser), nil
}
user_funcs.go
func Update(request *protos.UpdateRequest) (*store.User, error) {
claims, err := jwt_func.DecodeJwt(request.Token)
if err != nil {
return nil, err
}
filter := bson.D{{"_id", claims.Id}}
update := parseRequest(request)
err = repository.Update(filter, bson.D{{"$set", update}})
if err != nil {
return nil, err
}
user, err := repository.GetById(claims.Id)
return user, nil
}
func parseRequest(request *protos.UpdateRequest) bson.D {
var update bson.D
if request.Nickname != "" {
updateBson(&update, "nickname", request.Nickname)
}
if request.PhoneNumber != "" {
updateBson(&update, "phone_number", request.PhoneNumber)
}
if request.Email != "" {
updateBson(&update, "email", request.Email)
}
if request.Password != "" {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
updateBson(&update, "password", string(hashedPassword))
}
return update
}
func updateBson(data *bson.D, key string, value interface{}) {
*data = append(*data, bson.E{Key: key, Value: value})
}
user_repo.go
func Update(filter bson.D, updateData bson.D) error {
_, err := config.DBCollection.UpdateOne(config.DBContext, filter, updateData)
if err != nil {
config.ErrorConsoleLogger.Println("Error: couldn't update user")
return err
}
return nil
}
UserService.proto
message UpdateRequest {
string token = 1;
string nickname = 2;
string login = 3;
string email = 4;
string phoneNumber = 5;
string password = 6;
}
service UserService {
rpc Update(UpdateRequest) returns (GetResponse);
}
So what can be the problem? If you know, please tell me. I'd really appreciate it!

AES GCM decryption failed while decrypting data encrypted in flutter

I'm trying to decrypt the data in golang using the in-inbuild crypto library, the data is encrypted in flutter/dart using steel_crypt library.
The specific message that is thrown by the golang's crypto library is: panic: cipher: message authentication failed.
I'm running flutter app on Android amulator (localhost is: 10.0.2.2)
Golang version: go1.17.6 linux/amd64 |
Flutter version: Flutter 2.8.1 • channel stable |
Dart version: Dart 2.15.1
Golang Code
package main
import (
"bufio"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
)
func Encode(data []byte) string {
hb := base64.StdEncoding.EncodeToString([]byte(data))
return hb
}
// Decoding the base string to array of bytes
func Decode(data string) []byte {
hb, _ := base64.StdEncoding.DecodeString(data)
return hb
}
// Generating RSA private key
func GenerateRsaPrivateKey(size int) (*rsa.PrivateKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, size)
if err != nil {
return nil, err
}
return privateKey, nil
}
// Generating RSA public key
func GenerateRsaPublicKey(privateKey *rsa.PrivateKey) rsa.PublicKey {
return privateKey.PublicKey
}
// This function can be use encrypt a plain text with rsa algorithm
func RsaEncrypt(publicKey rsa.PublicKey, data string) ([]byte, error) {
encryptedBytes, err := rsa.EncryptPKCS1v15(
rand.Reader,
&publicKey,
[]byte(data))
return encryptedBytes, err
// encryptedBytes, err := rsa.EncryptOAEP(
// sha256.New(),
// rand.Reader,
// &publicKey,
// []byte(data),
// nil)
// return encryptedBytes, err
}
// This function can be use decrypt a encrypted text with rsa algorithm
func RsaDecrypt(privateKey rsa.PrivateKey, data []byte) ([]byte, error) {
decryptedBytes, err := privateKey.Decrypt(
nil,
data,
&rsa.OAEPOptions{Hash: crypto.SHA256})
return decryptedBytes, err
}
// This fucntion is used to dump/serialize the rsa public key
func DumpKey(key *rsa.PublicKey) ([]byte, error) {
return x509.MarshalPKCS1PublicKey(key), nil
}
// This function is used to load the rsa public key
func LoadKey(byteKey []byte) (*rsa.PublicKey, error) {
key, err := x509.ParsePKCS1PublicKey(byteKey)
return key, err
}
// Generate fixed size byte array
func GenerateAesKey(size int) []byte {
token := make([]byte, size)
rand.Read(token)
return token
}
// This fucntion can be used for encrypting a plain text using AES-GCM algorithm
func AesEncryption(key []byte, data string) ([]byte, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
cipherText := gcm.Seal(nonce, nonce, []byte(data), nil)
return cipherText, nil
}
// This fucntion can be used for decrypting the ciphertext encrypted using AES-GCM algorithm
func AesDecryption(key []byte, cipherText []byte, nonce []byte) ([]byte, error) {
c, err := aes.NewCipher(key)
if err != nil {
fmt.Println("1")
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
fmt.Println("2")
return nil, err
}
noncesize := gcm.NonceSize()
if len(cipherText) < noncesize {
fmt.Println("3")
return nil, err
}
cipherText = cipherText[noncesize:]
// nonce, cipherText := cipherText[:noncesize], cipherText[noncesize:]
plainText, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil {
fmt.Println("4", err.Error())
return nil, err
}
return plainText, nil
}
func ParseRsaPublicKeyFromPemStr(pubPEM string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pubPEM))
if block == nil {
return nil, errors.New("failed to parse PEM block containing the key")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
switch pub := pub.(type) {
case *rsa.PublicKey:
return pub, nil
default:
break // fall through
}
return nil, errors.New("Key type is not RSA")
}
type trans struct {
Key string `json:"key"`
}
type creden struct {
Data []byte `json:"data"`
Nonce []byte `json:"nonce"`
}
func startServer() {
fmt.Println("Starting Server...")
l, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer l.Close()
c, err := l.Accept()
if err != nil {
panic(err)
}
fmt.Println("Client Connected: ", c)
data, err := bufio.NewReader(c).ReadBytes('\n')
if err != nil {
panic(err)
}
var t trans
err_ := json.Unmarshal(data, &t)
if err != nil {
panic(err_)
}
// fmt.Println("Key: ", t.Key)
publicKey, e := ParseRsaPublicKeyFromPemStr(t.Key)
if e != nil {
panic(e)
}
// fmt.Println("Success", publicKey)
var cre creden
cre.Data = GenerateAesKey(32)
cre.Nonce = GenerateAesKey(12)
jsonRes, err := json.Marshal(cre)
if err != nil {
panic(err)
}
cipherText, e_r := RsaEncrypt(*publicKey, string(jsonRes))
if e_r != nil {
panic(e_r)
}
fmt.Println("cipherText: ", len(cipherText), " | ", cipherText)
encodedCipherText := Encode(cipherText)
n, err := c.Write([]byte(encodedCipherText))
if err != nil {
panic(err)
}
fmt.Println("Written back the response. Written Bytes: ", n)
data2, err2 := bufio.NewReader(c).ReadBytes('\n')
if err2 != nil {
panic(err2)
}
var t2 trans
err_2 := json.Unmarshal(data2, &t2)
if err_2 != nil {
panic(err_2)
}
fmt.Println("recv data: ", t2.Key)
cipherText2 := Decode(t2.Key)
fmt.Println("cipherText2: ", cipherText2)
plainText2, err := AesDecryption(cre.Data, cipherText2, cre.Nonce)
if err != nil {
panic(err)
}
fmt.Println("plainText: ", plainText2)
}
func main() {
startServer()
}
Flutter Code
External Libraries:
steel_crypt flutter pub add steel_crypt.
crypton flutter pub add crypton.
Just add below function in your flutter project and call it just before returning the MaterialApp(). If you call this function below this level it will get executed every time windows refres.
void connectFunc() async {
Socket socket = await Socket.connect('10.0.2.2', 8080);
print(socket);
print("Connected...");
// listen to the received data event stream
RSAKeypair rsaKeypair = RSAKeypair.fromRandom();
socket.listen((List<int> event) {
String base64Response = utf8.decode(event);
// dynamic response = base64.decode(base64Response);
// print(response.length);
// print(response.runtimeType);
try {
String plainText = rsaKeypair.privateKey.decrypt(base64Response);
print(plainText);
dynamic jsonData = jsonDecode(plainText);
String Key = jsonData["data"];
String Nonce = jsonData["nonce"];
var aes = AesCrypt(key: Key, padding: PaddingAES.pkcs7);
print('AES Symmetric GCM:');
var crypted = aes.gcm.encrypt(inp: 'words', iv: Nonce); //encrypt
// print(crypted);
// print(aes.gcm.decrypt(enc: crypted, iv: Nonce)); //decrypt
// print('');
print("send data: " + crypted);
dynamic dictData = {
"key": crypted,
};
socket.add(utf8.encode(jsonEncode(dictData) + "\n"));
} catch (e) {
print(e);
}
// dynamic response = jsonDecode(jsonResponse);
// String key = response["data"];
// String nonce = response["nonce"];
});
// RSAKeypair rsaKeypair = RSAKeypair.fromRandom();
dynamic dictData = {
"key": rsaKeypair.publicKey.toFormattedPEM(),
};
socket.add(utf8.encode(jsonEncode(dictData) + "\n"));
// send hello
// socket.add(utf8.encode('hello from flutter/dart'));
// return socket;
}
I have found the solution of the problem.
The solution is that we have to use another library called cryptography to encrypt the data in flutter/dart.
There is no error at golang side ( except some modifications ).
AesDecrypt function at the golang side have been modified
func AesDecryption(key []byte, cipherText []byte, nonce []byte) ([]byte, error) {
c, err := aes.NewCipher(key)
if err != nil {
fmt.Println("1")
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
fmt.Println("2")
return nil, err
}
// noncesize := gcm.NonceSize()
// if len(cipherText) < noncesize {
// fmt.Println("3")
// return nil, err
// }
// cipherText = cipherText[noncesize:]
// nonce, cipherText := cipherText[:noncesize], cipherText[noncesize:]
plainText, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil {
fmt.Println("4", err.Error())
return nil, err
}
return plainText, nil
}
Now the whole revised startServer function in here:
func startServer(nonce []byte, key []byte) {
fmt.Println("Starting Server...")
l, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer l.Close()
c, err := l.Accept()
if err != nil {
panic(err)
}
fmt.Println("Client Connected: ", c)
data, err := bufio.NewReader(c).ReadBytes('\n')
if err != nil {
panic(err)
}
var t trans
err_ := json.Unmarshal(data, &t)
if err != nil {
panic(err_)
}
// fmt.Println("Key: ", t.Key)
publicKey, e := ParseRsaPublicKeyFromPemStr(t.Key)
if e != nil {
panic(e)
}
// fmt.Println("Success", publicKey)
var cre creden
cre.Data = Encode(key) //GenerateAesKey(32)
cre.Nonce = Encode(nonce) //GenerateAesKey(12)
jsonRes, err := json.Marshal(cre)
if err != nil {
panic(err)
}
cipherText, e_r := RsaEncrypt(*publicKey, string(jsonRes))
if e_r != nil {
panic(e_r)
}
// fmt.Println("cipherText: ", len(cipherText), " | ", cipherText)
encodedCipherText := Encode(cipherText)
n, err := c.Write([]byte(encodedCipherText))
if err != nil {
panic(err)
}
fmt.Println("Written back the response. Written Bytes: ", n)
data2, err2 := bufio.NewReader(c).ReadBytes('\n')
if err2 != nil {
panic(err2)
}
var t2 trans
err_2 := json.Unmarshal(data2, &t2)
if err_2 != nil {
panic(err_2)
}
fmt.Println("recv data: ", t2.Key)
cipherText2 := Decode(t2.Key)
fmt.Println("cipherText2: ", cipherText2)
plainText2, err := AesDecryption(key, cipherText2, nonce)
if err != nil {
panic(err)
}
fmt.Println("plainText: ", string(plainText2))
}
And now the flutter side of solution:
external denepencies: cryptography: ^2.0.5
void connectFunc() async {
Socket socket = await Socket.connect('10.0.2.2', 8080);
print(socket);
print("Connected...");
// listen to the received data event stream
RSAKeypair rsaKeypair = RSAKeypair.fromRandom();
socket.listen((List<int> event) async {
String base64Response = utf8.decode(event);
try {
String plainText = rsaKeypair.privateKey.decrypt(base64Response);
dynamic jsonData = jsonDecode(plainText);
String Key = jsonData["data"];
String Nonce = jsonData["nonce"];
String message = "Hello World";
final algorithm = AesGcm.with256bits();
final secretBox = await algorithm.encrypt(
message.codeUnits,
secretKey: SecretKey(base64.decode(jsonData["data"])),
nonce: base64.decode(jsonData["nonce"]),
);
print("key: ${base64.decode(jsonData["data"])}");
print("nonce: ${base64.decode(jsonData["nonce"])}");
print("secretBox: ${secretBox.concatenation(nonce: false)}");
dynamic dictData = {
"key": base64.encode(secretBox.concatenation(nonce: false)),
};
socket.add(utf8.encode(jsonEncode(dictData) + "\n"));
} catch (e) {
print(e);
}
});
dynamic dictData = {
"key": rsaKeypair.publicKey.toFormattedPEM(),
};
socket.add(utf8.encode(jsonEncode(dictData) + "\n"));
}

Converting a struct to a bson document

I have a struct that looks like this:
type User struct {
UserID string `bson:"user_id"`
Name string `bson:"name"`
Address string `bson:"address"`
}
I am using mongo's UpdateOne to only update specific fields in a document. Doing this allows me to only update the name where the user_id is 1234:
filter := bson.D{{"user_id", "1234"}}
update := bson.D{{"$set",
bson.D{
{"name", "john"},
},
}}
myCollection.UpdateOne(context.Background(), filter, update)
However, I want to use a struct instead to replace whatever is in the update variable.
So I want to be able use,
update:= User{Name: "john"}
How do I convert this to a bson document like in the working example?
Most responses always use bson.Marshal() followed by bson.Unmarshal(), but this form is slower.
Imagine that you are on a server with thousands of accesses to the database per second. Your code can slow down server operation depending on the quality of the code.
For this reason, I always do the benchmark tests before choosing which method to use.
An example is shown below:
package bsontest
import (
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"reflect"
"testing"
)
type User struct {
UniqueID string `bson:"_id"`
Name string `bson:"name"`
Password string `bson:"password"`
Email string `bson:"email"`
Age int `bson:"age"`
Admin bool `bson:"admin"`
}
func (e *User) FromBson(data bson.M) (err error) {
var tagValue string
element := reflect.ValueOf(e).Elem()
for i := 0; i < element.NumField(); i += 1 {
typeField := element.Type().Field(i)
tag := typeField.Tag
tagValue = tag.Get("bson")
if tagValue == "-" {
continue
}
switch element.Field(i).Kind() {
case reflect.String:
switch data[tagValue].(type) {
case string:
element.Field(i).SetString(data[tagValue].(string))
default:
err = errors.New(tagValue+" must be a string")
return
}
case reflect.Bool:
switch data[tagValue].(type) {
case bool:
element.Field(i).SetBool(data[tagValue].(bool))
default:
err = errors.New(tagValue+" must be a boolean")
return
}
case reflect.Int:
switch data[tagValue].(type) {
case int:
element.Field(i).SetInt(int64(data[tagValue].(int)))
case int64:
element.Field(i).SetInt(data[tagValue].(int64))
default:
err = errors.New(tagValue+" must be a integer")
return
}
}
}
return
}
func (e *User) ToBson() (data bson.M) {
var tagValue string
data = bson.M{}
element := reflect.ValueOf(e).Elem()
for i := 0; i < element.NumField(); i += 1 {
typeField := element.Type().Field(i)
tag := typeField.Tag
tagValue = tag.Get("bson")
if tagValue == "-" {
continue
}
switch element.Field(i).Kind() {
case reflect.String:
value := element.Field(i).String()
data[tagValue] = value
case reflect.Bool:
value := element.Field(i).Bool()
data[tagValue] = value
case reflect.Int:
value := element.Field(i).Int()
data[tagValue] = value
}
}
return
}
var user User
func init() {
user = User{
UniqueID: "12345-67890-ABCDE-FGHIJKL",
Name: "Fulano da Silva Sauro",
Password: "pangea",
Email: "sauro#pangea.com",
Admin: true,
}
}
func ExampleUser_ToBson() {
var err error
var userAsBSon = user.ToBson()
fmt.Printf("%+v\n", userAsBSon)
user = User{}
fmt.Printf("%+v\n", user)
err = user.FromBson(userAsBSon)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", user)
// Output:
// map[_id:12345-67890-ABCDE-FGHIJKL admin:true age:0 email:sauro#pangea.com name:Fulano da Silva Sauro password:pangea]
// {UniqueID: Name: Password: Email: Age:0 Admin:false}
// {UniqueID:12345-67890-ABCDE-FGHIJKL Name:Fulano da Silva Sauro Password:pangea Email:sauro#pangea.com Age:0 Admin:true}
}
func Benchmark_UsingMarshalAndUnmarshal(b *testing.B) {
var err error
var bsonAsByte []byte
var bsonData bson.M
for i := 0; i < b.N; i++ {
bsonAsByte, err = bson.Marshal(&user)
if err != nil {
panic(err)
}
err = bson.Unmarshal(bsonAsByte, &bsonData)
if err != nil {
panic(err)
}
}
}
func Benchmark_UsingUnmarshalAndMarshal(b *testing.B) {
var err error
var bsonAsByte []byte
var bsonData = bson.M{
"_id":"12345-67890-ABCDE-FGHIJKL",
"admin":true,
"age":0,
"email":"sauro#pangea.com",
"name":"Fulano da Silva Sauro",
"password":"pangea",
}
for i := 0; i < b.N; i++ {
bsonAsByte, err = bson.Marshal(&bsonData)
if err != nil {
panic(err)
}
err = bson.Unmarshal(bsonAsByte, &user)
if err != nil {
panic(err)
}
}
}
func Benchmark_UsingToBson(b *testing.B) {
var err error
var bsonData = bson.M{
"_id":"12345-67890-ABCDE-FGHIJKL",
"admin":true,
"age":0,
"email":"sauro#pangea.com",
"name":"Fulano da Silva Sauro",
"password":"pangea",
}
for i := 0; i < b.N; i++ {
err = user.FromBson(bsonData)
if err != nil {
panic(err)
}
}
}
func Benchmark_UsingFromBson(b *testing.B) {
var bsonData bson.M
for i := 0; i < b.N; i++ {
bsonData = user.ToBson()
}
_ = bsonData
}
Times:
Benchmark_UsingMarshalAndUnmarshal
Benchmark_UsingMarshalAndUnmarshal-8 398800 2996 ns/op
Benchmark_UsingToBson
Benchmark_UsingToBson-8 1789176 643.0 ns/op
Benchmark_UsingFromBson
Benchmark_UsingFromBson-8 2128242 539.6 ns/op
Benchmark_UsingUnmarshalAndMarshal
Benchmark_UsingUnmarshalAndMarshal-8 474501 2524 ns/op

DRY out my Go function with interfaces

I have the following function:
func (r *Resource) Create(kind string, data io.ReadCloser) (err error) {
decoder := json.NewDecoder(data)
r.Kind = kind
switch kind {
case "user":
var user User
if err = decoder.Decode(&user); err != nil {
panic(err)
}
if err = user.Save(r.Context); err != nil {
panic(err)
}
r.Data = user
break
case "space":
var space Space
if err = decoder.Decode(&space); err != nil {
panic(err)
}
if err = space.Save(r.Context); err != nil {
panic(err)
}
r.Data = space
break
case "room":
var room Room
if err = decoder.Decode(&room); err != nil {
panic(err)
}
if err = room.Save(r.Context); err != nil {
panic(err)
}
r.Data = room
break
case "element":
var element Element
if err = decoder.Decode(&element); err != nil {
panic(err)
}
if err = element.Save(r.Context); err != nil {
panic(err)
}
r.Data = element
break
default:
break
}
return
}
As you can see, each case in the switch is identical except for the type of the struct that receives the JSON data.
I suspect that there's an answer in interfaces and type assertion.
EDIT:
I was able to break out the saving part into a separate method, but I still can't figure out a good way to decode the JSON object into the appropriate struct without the switch statement.
func (r *Resource) Create(kind string, data io.ReadCloser) (err error) {
decoder := json.NewDecoder(data)
r.Kind = kind
switch kind {
case "user":
var user User
if err = decoder.Decode(&user); err != nil {
panic(err)
}
r.saveEntity(&user)
break
case "space":
var space Space
if err = decoder.Decode(&space); err != nil {
panic(err)
}
r.saveEntity(&space)
break
case "room":
var room Room
if err = decoder.Decode(&room); err != nil {
panic(err)
}
r.saveEntity(&room)
break
case "element":
var element Element
if err = decoder.Decode(&element); err != nil {
panic(err)
}
r.saveEntity(&element)
break
default:
break
}
return
}
func (r *Resource) saveEntity(e Entity) {
if err := e.Save(r.Context); err != nil {
panic(err)
}
r.Data = e
}
You could move the instantiation to one-line functions and create a mapping which maps the
kind to the respective instantiation function. The rest of the code should be re-usable.
Example:
kinds := map[string]func() Entity {
"user": func() Entity { return &User{} },
"space": func() Entity { return &Space{} },
"room": func() Entity { return &Room{} },
}
func Create(kind string) {
instance := kinds[kind]()
decoder.Decode(instance)
saveEntity(instance)
}