benchmarks protojson + http.json; added repo integration test
This commit is contained in:
parent
3237dcbc2a
commit
fb1dff23b9
45
README.md
45
README.md
|
@ -14,14 +14,57 @@ Why? Separation of concerns; do not expose database/domain internals. Structs in
|
||||||
- JSON mapping: https://protobuf.dev/programming-guides/proto3/#json
|
- JSON mapping: https://protobuf.dev/programming-guides/proto3/#json
|
||||||
- `Pokemons` as object with `entries` is ugly but see https://github.com/golang/protobuf/issues/675 - no way to convert a slice to a protobuf message...
|
- `Pokemons` as object with `entries` is ugly but see https://github.com/golang/protobuf/issues/675 - no way to convert a slice to a protobuf message...
|
||||||
|
|
||||||
|
Is this over-engineered or what? On top of that, protojson's performance is much worse:
|
||||||
|
|
||||||
|
```
|
||||||
|
goos: darwin
|
||||||
|
goarch: arm64
|
||||||
|
pkg: pokedex/rest
|
||||||
|
BenchmarkJson-8 1275931 928.9 ns/op
|
||||||
|
BenchmarkJson-8 1292184 926.6 ns/op
|
||||||
|
BenchmarkJson-8 1292647 926.0 ns/op
|
||||||
|
BenchmarkJson-8 1291228 926.3 ns/op
|
||||||
|
BenchmarkJson-8 1294453 927.3 ns/op
|
||||||
|
BenchmarkProtoJson-8 772567 1510 ns/op
|
||||||
|
BenchmarkProtoJson-8 766005 1517 ns/op
|
||||||
|
BenchmarkProtoJson-8 779281 1512 ns/op
|
||||||
|
BenchmarkProtoJson-8 774534 1513 ns/op
|
||||||
|
BenchmarkProtoJson-8 771370 1511 ns/op
|
||||||
|
PASS
|
||||||
|
ok pokedex/rest 16.500s
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's **with indent**! Without:
|
||||||
|
|
||||||
|
```
|
||||||
|
BenchmarkJson-8 4067257 266.6 ns/op
|
||||||
|
BenchmarkJson-8 4503345 266.5 ns/op
|
||||||
|
BenchmarkJson-8 4512295 266.9 ns/op
|
||||||
|
BenchmarkJson-8 4505510 266.7 ns/op
|
||||||
|
BenchmarkJson-8 4482391 267.4 ns/op
|
||||||
|
```
|
||||||
|
|
||||||
|
The question then becomes: why use a `.proto` file to exchange a contract at all, if you're not using gRPC?
|
||||||
|
|
||||||
## Swagger
|
## Swagger
|
||||||
|
|
||||||
- Exposed at `http://localhost:8080/docs`
|
- Exposed at `http://localhost:8080/docs`
|
||||||
- Regenerate with `swag init`, see https://github.com/swaggo/http-swagger
|
- Regenerate with `swag init`, see https://github.com/swaggo/http-swagger
|
||||||
- Annotation format: see https://github.com/swaggo/swag
|
- Annotation format: see https://github.com/swaggo/swag
|
||||||
|
|
||||||
|
Big bummer: annotations contain endpoint duplication...
|
||||||
|
|
||||||
## Optimizing the binary
|
## Optimizing the binary
|
||||||
|
|
||||||
See `Makefile`; use https://upx.github.io/ to package after stripping some debug info.
|
See `Makefile`; use https://upx.github.io/ to package after stripping some debug info.
|
||||||
|
|
||||||
Somehow doesn't work on OSX (process killed)?
|
Somehow doesn't work on OSX (process killed)?
|
||||||
|
|
||||||
|
## Interesting philosophical questions
|
||||||
|
|
||||||
|
1. Where should routing be defined? Central or with the domain package (which doesn't seem to be the idiomatic Go way?)
|
||||||
|
2. To use or not use DTO or response objects?
|
||||||
|
3. How to handle dependencies without dragging in _Yet Another Unneeded Framework_?
|
||||||
|
4. Custom error wrapping or not?
|
||||||
|
5. Custom Go web frameworks that ease muxing and exponential back-offs or not?
|
||||||
|
6. Where should db migration go? Reusability in tests?
|
3
go.mod
3
go.mod
|
@ -3,6 +3,7 @@ module pokedex
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/swaggo/http-swagger v1.3.4
|
github.com/swaggo/http-swagger v1.3.4
|
||||||
github.com/swaggo/swag v1.16.3
|
github.com/swaggo/swag v1.16.3
|
||||||
google.golang.org/protobuf v1.33.0
|
google.golang.org/protobuf v1.33.0
|
||||||
|
@ -12,6 +13,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
@ -21,6 +23,7 @@ require (
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/swaggo/files v1.0.1 // indirect
|
github.com/swaggo/files v1.0.1 // indirect
|
||||||
golang.org/x/net v0.24.0 // indirect
|
golang.org/x/net v0.24.0 // indirect
|
||||||
golang.org/x/tools v0.20.0 // indirect
|
golang.org/x/tools v0.20.0 // indirect
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package pokemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pokeTest(name string) pokemon {
|
||||||
|
return pokemon{
|
||||||
|
Name: name,
|
||||||
|
Weight: 124,
|
||||||
|
Height: 356,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepo_Find(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
label string
|
||||||
|
searchFor string
|
||||||
|
pokes []pokemon
|
||||||
|
expectResult pokemon
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
label: "No pokes in db",
|
||||||
|
searchFor: "Jos",
|
||||||
|
pokes: []pokemon{},
|
||||||
|
expectResult: pokemon{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "No pokes matching the name in db",
|
||||||
|
searchFor: "Jos",
|
||||||
|
pokes: []pokemon{
|
||||||
|
pokeTest("Snul"),
|
||||||
|
pokeTest("Muesli"),
|
||||||
|
pokeTest("Bonnie"),
|
||||||
|
},
|
||||||
|
expectResult: pokemon{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Unique poke name found",
|
||||||
|
searchFor: "Jos",
|
||||||
|
pokes: []pokemon{
|
||||||
|
pokeTest("Jos"),
|
||||||
|
pokeTest("Miel"),
|
||||||
|
},
|
||||||
|
expectResult: pokeTest("Jos"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.label, func(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
db.AutoMigrate(&pokemon{})
|
||||||
|
repo := NewRepo(db)
|
||||||
|
|
||||||
|
for _, p := range tc.pokes {
|
||||||
|
db.Save(&p)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := repo.Find(tc.searchFor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectResult.Name, result.Name)
|
||||||
|
assert.Equal(t, tc.expectResult.Weight, result.Weight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Json(w http.ResponseWriter, data any) {
|
func Json(w http.ResponseWriter, data any) {
|
||||||
bytes, err := json.MarshalIndent(data, "", " ")
|
bytes, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Oops, something went wrong", http.StatusInternalServerError)
|
http.Error(w, "Oops, something went wrong", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"pokedex/pb"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmptyResponseWriter struct{}
|
||||||
|
|
||||||
|
func (w EmptyResponseWriter) Header() http.Header {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (w EmptyResponseWriter) Write([]byte) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
func (w EmptyResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func pokeTest() *pb.Pokemons {
|
||||||
|
return &pb.Pokemons{
|
||||||
|
Entries: []*pb.Pokemon{
|
||||||
|
{
|
||||||
|
Name: "Charizard",
|
||||||
|
Weight: 124,
|
||||||
|
Moves: []*pb.Move{
|
||||||
|
{
|
||||||
|
Name: "Squirt",
|
||||||
|
Url: "https://boemklets.co",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkJson(b *testing.B) {
|
||||||
|
writer := EmptyResponseWriter{}
|
||||||
|
pokemon := pokeTest()
|
||||||
|
|
||||||
|
for i := 0; i <= b.N; i++ {
|
||||||
|
Json(writer, pokemon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkProtoJson(b *testing.B) {
|
||||||
|
writer := EmptyResponseWriter{}
|
||||||
|
pokemon := pokeTest()
|
||||||
|
|
||||||
|
for i := 0; i <= b.N; i++ {
|
||||||
|
ProtoJson(writer, pokemon)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue