
Introduction
Especially in multi-threaded applications it can be necessary to synchronize properties between objects, or at least be notified of changes on certain properties. This is where the Binding Properties comes in, which is basically a form of the Observer pattern.
In this pattern, observers can subscribe to a special ‘event’-handler on an object. When a property changes, all the subscribers, if any, will be notified.
The flow is as follows:
- An object with bindable properties is created
- One or more observers subscribe to this object
- If a property changes in this object, the subscribers get notified and handle the change accordingly.
Implementation in Go
In this example we will build a simple Person struct. In our world, a person just has a name and an age. This struct has a number, or as the case may be no, observers which will be notified any time a property changes:
package main
import (
"fmt"
"strconv"
"sync"
)
type Observer func(propertyName, newValue string)
type Person struct {
name string
age int
observers []Observer
mu sync.Mutex
}
func NewPerson(name string, age int) *Person {
return &Person{
name: name,
age: age,
}
}
func (p *Person) GetName() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.name
}
func (p *Person) SetName(name string) {
p.mu.Lock()
p.name = name
p.mu.Unlock()
p.notifyObservers("name", name)
}
func (p *Person) GetAge() int {
p.mu.Lock()
defer p.mu.Unlock()
return p.age
}
func (p *Person) SetAge(age int) {
p.mu.Lock()
p.age = age
p.mu.Unlock()
p.notifyObservers("age", strconv.Itoa(age))
}
func (p *Person) Subscribe(observer Observer) {
p.mu.Lock()
defer p.mu.Unlock()
p.observers = append(p.observers, observer)
}
func (p *Person) notifyObservers(property, value string) {
p.mu.Lock()
defer p.mu.Unlock()
for _, observer := range p.observers {
observer(property, value)
}
}
A few notes:
- We use
sync.Mutex, a standard library function in Go, to protect shared data from concurrent access. - The
observersslice holds the list of observers, and we use the mutex to synchronize access to this slice. - In the two
Getlock the mutex to guarantee exclusive access. We then use thedeferkeyword to make sure the mutex is unlocked, even in the case of a panic. - In the two
Setmethods we lock the mutex as well, set the appropiate value, and then unlock the mutex. We do this before calling thenotifyObservers()method, informing the observers about the change. We do this to avoid a possible deadlock, by not holding a lock while we call the observers. - In the
Subscribe()method we add an observer in a thread-safe manner, by locking and unlocking our mutex. - For the
notifyObservers()method, which is called from the twoSetmethods, we lock the mutex to ensure the notification is thread-safe. Again, we use thedefermethod to unlock the mutex, even if the method panics.
Testing time
Now we can test our setup:
func main() {
person := NewPerson("Test", 55)
person.Subscribe(func(property, value string) {
fmt.Printf("main thread: %s changed to %s\n", property, value)
})
go func() {
person.SetName("Jane")
person.SetAge(21)
person.Subscribe(func(property, value string) {
fmt.Printf("goroutine: %s changed to %s\n", property, value)
})
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
person.SetName("John")
}()
person.SetAge(32)
wg.Wait()
}
Here you can see we simulate concurrent updates to the Person struct, by using a goroutine. We use a sync.WaitGroup to wait for the goroutine to finish execution before the program terminates.
Conclusion
Implementing this pattern in Go is quite straightforward, using the sync package. The code is also quite clear. One possible enhancement could be to set a different subscriber for each property, so that our pattern becomes a bit more fine-grained. However, that is the subject for a next post.