Skip to content

Commit bd98f33

Browse files
committed
code improvements; removed deprecated flags; added perf metrics; added interface gen support
1 parent fb7a587 commit bd98f33

13 files changed

Lines changed: 976 additions & 300 deletions

README.md

Lines changed: 116 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,21 @@ person := Person{
5656

5757
## Installation
5858

59-
Install **gobetter** and its dependency:
59+
Install **gobetter** as standalone utility:
6060

6161
```bash
62-
# Install goimports (for code formatting)
63-
go install golang.org/x/tools/cmd/goimports@latest
64-
65-
# Install gobetter
6662
go install github.com/mobiletoly/gobetter@latest
6763
```
6864

65+
or (even better) install **gobetter** to use as tool in your go.mod:
66+
67+
```
68+
tool (
69+
github.com/mobiletoly/gobetter
70+
)
71+
```
72+
73+
6974
## Quick Start
7075

7176
### 1. Annotate Your Structs
@@ -75,52 +80,64 @@ Add annotations to your Go structs:
7580
```go
7681
package main
7782

83+
// Put on top of the file
7884
//go:generate gobetter -input $GOFILE
7985

8086
type Person struct { //+gob:Constructor
81-
firstName string //+gob:getter
82-
lastName string //+gob:getter
83-
dob string //+gob:getter +gob:acronym
84-
Score int
85-
Description string //+gob:_
87+
FirstName string
88+
LastName string
89+
email string //+gob:getter
90+
dob string //+gob:getter +gob:acronym
91+
Score int
92+
Description string //+gob:_
8693
}
8794
```
8895

96+
IMPORTANT: +gob:getter annotation must be used for private fields (starting with lowercase letter)
97+
only.
98+
8999
### 2. Generate Code
90100

91-
Run the generator:
101+
Run the generator to generate all annotated structs:
92102

93103
```bash
94104
go generate ./...
95105
```
96106

107+
it will result in creating files with suffix `_gob.go` for each of your file that contains
108+
annotated structs.
109+
97110
### 3. Use the Generated Builder
98111

99112
```go
100113
person := NewPersonBuilder().
101114
FirstName("John").
102115
LastName("Doe").
116+
Email("john.doe@example.com").
103117
DOB("01/01/1990").
104118
Score(85).
105119
Build()
106120

107121
// Set optional fields after building
108122
person.Description = "Software engineer"
109123

110-
// Access private fields through getters
111-
fmt.Println(person.FirstName()) // "John"
112-
fmt.Println(person.DOB()) // "01/01/1990" (acronym handling)
124+
fmt.Println(person.FirstName) // "John"
125+
fmt.Println(person.LastName) // "Doe"
126+
fmt.Println(person.Email()) // "john.doe@example.com" (getter function to call from outside)
127+
fmt.Println(person.DOB()) // "01/01/1990" (getter function, acronym is DOB instead of dob)
128+
fmt.Println(person.Score) // 85 (public field, no function needed)
129+
fmt.Println(person.Description) // "Software engineer"
113130
```
114131

115132
## 📝 Annotations Reference
116133

117-
| Annotation | Description | Example |
118-
|------------|-------------|---------|
119-
| `//+gob:Constructor` | Generate builder for struct | `type Person struct { //+gob:Constructor` |
120-
| `//+gob:constructor` | Generate package-level builder | `type person struct { //+gob:constructor` |
121-
| `//+gob:getter` | Generate getter for private field | `name string //+gob:getter` |
122-
| `//+gob:acronym` | Treat field as acronym (DOB vs Dob) | `dob string //+gob:getter +gob:acronym` |
123-
| `//+gob:_` | Mark field as optional (skip in builder) | `description string //+gob:_` |
134+
| Annotation | Description | Example |
135+
|----------------------|------------------------------------------|-------------------------------------------|
136+
| `//+gob:Constructor` | Generate builder for struct | `type Person struct { //+gob:Constructor` |
137+
| `//+gob:constructor` | Generate package-level builder | `type person struct { //+gob:constructor` |
138+
| `//+gob:getter` | Generate getter for private field | `name string //+gob:getter` |
139+
| `//+gob:acronym` | Treat field as acronym (DOB vs Dob) | `dob string //+gob:getter +gob:acronym` |
140+
| `//+gob:_` | Mark field as optional (skip in builder) | `description string //+gob:_` |
124141

125142
## 🏗️ Nested Structs Support
126143

@@ -155,16 +172,22 @@ config := NewConfigBuilder().
155172
Build()
156173
```
157174

175+
Run:
176+
177+
```bash
178+
go generate ./...
179+
go test -bench . -benchmem -run ^$ -count 5 ./...
180+
```
181+
158182
## Configuration Options
159183

160184
### Command Line Flags
161185

162-
| Flag | Values | Description |
163-
|------|--------|-------------|
164-
| `-input` | `<file>` | Input file to process |
165-
| `-output` | `<file>` | Output file (default: `<input>_gob.go`) |
166-
| `-generate-for` | `all\|exported\|annotated` | Which structs to process |
167-
| `-constructor` | `exported\|package\|none` | Constructor visibility level |
186+
| Flag | Values | Description |
187+
|-----------------|----------------------------|------------------------------|
188+
| `-input` | `<path>` | Input file or directory |
189+
| `-generate-for` | `all\|exported\|annotated` | Which structs to process |
190+
| `-constructor` | `exported\|package\|none` | Constructor visibility level |
168191

169192
### Examples
170193

@@ -179,11 +202,11 @@ gobetter -input=models.go -generate-for=exported
179202
gobetter -input=models.go -generate-for=all -constructor=package
180203
```
181204

182-
## IDE Integration
205+
## Optional IDE Integration
183206

184207
### IntelliJ IDEA / GoLand
185208

186-
Set up a File Watcher for automatic generation:
209+
Set up a File Watcher for automatic generation (no need to run `go generate`):
187210

188211
1. Go to **Preferences → Tools → File Watchers**
189212
2. Add **Custom** watcher:
@@ -193,4 +216,67 @@ Set up a File Watcher for automatic generation:
193216
- **Arguments**: `generate`
194217
- **Scope**: Create scope with pattern `file:*.go&&!file:*_gob.go`
195218

196-
Now builders regenerate automatically when you save Go files!
219+
Now builders regenerate automatically when you save Go files
220+
221+
222+
## Performance
223+
224+
**Summary:** For typical structs, gobetter’s step-builder is *as fast* as direct struct
225+
initialization and has the *same* allocation profile.
226+
227+
- **CPU:** Direct literal ~19.8–20.8 ns/op; Builder chain ~20.1–24.1 ns/op in our latest run (
228+
averages ≈ **20.18 ns/op** vs **21.64 ns/op**, respectively). The single-digit ns delta is within
229+
typical microbenchmark variance and both approaches remain essentially equivalent for real
230+
workloads.
231+
- **Allocations:** **1 alloc/op** (the single `*T` instance you ultimately build), **~96 B/op** for
232+
the `Person` example. Step structs are tiny (a single pointer to the root) and stay on the **stack
233+
**.
234+
- **Why it’s fast:**
235+
- Setters are trivial field assignments that the compiler **inlines**.
236+
- Step structs are returned **by value** and typically do **not escape** (escape analysis keeps
237+
them on stack).
238+
- `Build()` returns the same `*T` allocated once at the start of the chain — identical to
239+
`&T{...}`.
240+
241+
### Reproduce the benchmark
242+
243+
```go
244+
func BenchmarkDirectLiteral(b *testing.B) {
245+
b.ReportAllocs()
246+
for i := 0; i < b.N; i++ {
247+
p := &Person{firstName: "John", lastName: "Doe", dob: tDOB, Email: "john.doe@example.com"}
248+
sink = p
249+
}
250+
}
251+
252+
func BenchmarkBuilderChain(b *testing.B) {
253+
b.ReportAllocs()
254+
for i := 0; i < b.N; i++ {
255+
p := NewPersonBuilder().
256+
FirstName("John").
257+
LastName("Doe").
258+
DOB(tDOB).
259+
Email("john.doe@example.com").
260+
GobFinalizer().
261+
Build()
262+
sink = p
263+
}
264+
}
265+
```
266+
267+
**Sample results (one machine):**
268+
269+
```
270+
BenchmarkDirectLiteral-12 57792015 20.02 ns/op 96 B/op 1 allocs/op
271+
BenchmarkDirectLiteral-12 59628193 19.77 ns/op 96 B/op 1 allocs/op
272+
BenchmarkDirectLiteral-12 59373484 20.78 ns/op 96 B/op 1 allocs/op
273+
BenchmarkDirectLiteral-12 57435504 19.94 ns/op 96 B/op 1 allocs/op
274+
BenchmarkDirectLiteral-12 55259538 20.38 ns/op 96 B/op 1 allocs/op
275+
BenchmarkBuilderChain-12 58040876 20.08 ns/op 96 B/op 1 allocs/op
276+
BenchmarkBuilderChain-12 57155670 21.04 ns/op 96 B/op 1 allocs/op
277+
BenchmarkBuilderChain-12 49360202 22.16 ns/op 96 B/op 1 allocs/op
278+
BenchmarkBuilderChain-12 54273560 24.14 ns/op 96 B/op 1 allocs/op
279+
BenchmarkBuilderChain-12 55646794 20.80 ns/op 96 B/op 1 allocs/op
280+
```
281+
282+
(Results vary by CPU/Go version and flags; use multiple runs with `-count` for stability.)

benchmark_test.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func BenchmarkGenerateCode(b *testing.B) {
1212
// Create a test input file
1313
inputContent := `package test
1414
15-
//go:generate gobetter -input $GOFILE
15+
//go:generate` + /*to avoid go:generate comment being executed*/ `gobetter -input $GOFILE
1616
1717
type Person struct { //+gob:Constructor
1818
firstName string //+gob:getter
@@ -51,10 +51,8 @@ type Company struct { //+gob:Constructor
5151
}
5252

5353
config := &Config{
54-
InputFile: inputFile,
55-
OutputFile: filepath.Join(tmpDir, "test_gob.go"),
54+
InputPath: inputFile,
5655
GenerateFor: nil,
57-
UsePtrReceiver: false,
5856
ConstructorVisibility: ConstructorExported,
5957
}
6058

@@ -72,7 +70,6 @@ func BenchmarkStructFieldGeneration(b *testing.B) {
7270
sf := &StructField{
7371
StructFlags: &StructFlags{
7472
ProcessStruct: true,
75-
PtrReceiver: false,
7673
Visibility: ExportedVisibility,
7774
},
7875
StructName: "Person",

constants.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const (
1616
// Command line flag names
1717
const (
1818
FlagInput = "input"
19-
FlagOutput = "output"
2019
FlagGenerateFor = "generate-for"
2120
FlagReceiver = "receiver"
2221
FlagConstructor = "constructor"

example/person_bench_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package example
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
var (
9+
sink *Person
10+
tDOB = time.Date(1990, 7, 1, 0, 0, 0, 0, time.UTC)
11+
)
12+
13+
func BenchmarkDirectLiteral(b *testing.B) {
14+
b.ReportAllocs()
15+
for i := 0; i < b.N; i++ {
16+
p := &Person{
17+
firstName: "John",
18+
lastName: "Doe",
19+
dob: tDOB,
20+
Email: "john@example.com",
21+
// Phone, Bio left nil (optional)
22+
}
23+
// prevent compiler from optimizing it away
24+
sink = p
25+
}
26+
}
27+
28+
func BenchmarkBuilderChain(b *testing.B) {
29+
b.ReportAllocs()
30+
for i := 0; i < b.N; i++ {
31+
p := NewPersonBuilder().
32+
FirstName("John").
33+
LastName("Doe").
34+
DOB(tDOB).
35+
Email("john@example.com").
36+
Build()
37+
sink = p
38+
}
39+
}

example/structs.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ import (
44
"time"
55
)
66

7-
//go:generate gobetter -input $GOFILE
7+
//go:generate ../gobetter -input $GOFILE
8+
9+
type S struct { //+gob:Constructor
10+
Identifier string
11+
IO interface {
12+
Read([]byte) (int, error)
13+
Close() error
14+
}
15+
}
816

917
// Person represents a person with basic information
1018
type Person struct { //+gob:Constructor

0 commit comments

Comments
 (0)