如何干净地处理结构状态错误?
So I'm just getting my feet wet with Go, and I'm trying to cleanly handle errors in the Go-way. One result is that having a type Movie struct
with methods to update the record, and sync to a data store. So an example method:
func (movie Movie) SetTitle(title string) : error {
prevTitle := movie.Title
movie.Title = title
json, err := json.Marshal(movie)
if (err != nil) {
movie.Title = prevTitle
return err
}
err = db.SetValue(movie.id, json)
if (err != nil) {
movie.Title = prevTitle
return err
}
return nil
}
The issue above is the 'cleanup' for a failed operation is to save the previous state, and restore as appropriate. But this feels very unmaintainable - it's so easy for a new error check to be made in the future, only to return without the proper cleanup. This would get even worse with multiple such 'cleanups'.
So to contrast, in other languages, I would wrap the whole bit in a try/catch, and handle cleanup just inside a single catch block. Here, I can:
- Create a copy of
movie
, do all the stuff on the copy, and then only copy back to the original object once everything is successful - Change the logic to:
if json, err := json.Marshal(movie); err == nil {
if err = db.SetValue(...); err == nil {
return nil
}
}
movie.Title = prevTitle;
return err
Which works, but I don't love having a level of nesting per check.
- Change the error return to indicate it was updated locally, but not saved
- Do it as described above
- Break out the save logic into an
func Update() : err
function to minimize the number of checks needed (just thought of this - think I like this one)
I feel like none of these are perfect; am I missing something obvious?
所以我刚开始接触Go,而我正努力清理Go中的错误 -方式。 结果是拥有 上面的问题是失败操作的“清理”是保存先前的状态,并适当地还原。 但这感觉很难维护-将来很容易进行新的错误检查,而返回时却没有适当的清理就很容易。 如果进行多次此类“清理”,情况将变得更加糟糕。 p>
因此,相比之下,在其他语言中,我会将整个位包装在try / catch中,并在单个catch块中处理清理操作。 在这里,我可以: p>
这行得通,但我不知道 p>
我觉得这些都不是完美的; 我缺少明显的东西吗? p>
div> Movie struct code>类型的电影,并带有更新记录和同步到数据存储的方法。 因此是一个示例方法: p>
func(电影电影)SetTitle(标题字符串):error {
prevTitle:= movie.Title
movie.Title = title
json ,err:= json.Marshal(movie)
if(err!= nil){
movie.Title = prevTitle
return err
}
err = db.SetValue(movie.id,json)
if(err!= nil){
movie.Title = prevTitle
return err
}
return nil
}
code> pre>
电影 code>的副本,执行副本上的所有内容,然后仅复制回原始内容 一切成功后立即成为对象 li>
如果json,err:= json.Marshal(movie ); err == nil {
如果err = db.SetValue(...); err == nil {
return nil
}
}
movie.Title = prevTitle;
return err
code> pre>
func Update():err code>函数,以最大程度地减少所需的检查次数(仅考虑到 li>
ol>
Update
The way you are persisting can cause numerous problems:
- you could incandescently mutate original object
- you mix different layers in one method, it makes code very fragile
- method, does too much
I suggest, separate update and persistence. Playground
type Movie struct {
id int
Title string
}
func (m *Movie) Persist() error {
json, err := json.Marshal(m)
if err != nil {
return err
}
log.Printf("Storing in db: %s", json)
return nil
}
func main() {
m := &Movie{1, "Matrix"}
m.Title = "Iron Man"
if err := m.Persist(); err != nil {
log.Fatalln(err)
}
}
Old answer
In your example you use by-value receiver. In this case, you get a copy of the struct in the method, you free to modify that copy, but all changes will not be visible outside.
func (movie Movie) SetTitleValueSemantic(title string) error {
movie.Title = title
json, err := json.Marshal(movie)
if err != nil {
return err
}
log.Printf("Serialized: %s", json)
return nil
}
playgorund: https://play.golang.org/p/mVnQ66TCaG9
I would strongly recommend avoiding such a coding style. In case you REALLY need some of this kind, here is an example for inspiration:
playground: https://play.golang.org/p/rHacnsRLkEE
func (movie *Movie) SetTitle(title string) (result *Movie, e error) {
movieCopy := new(Movie)
*movieCopy = *movie
result = movie
defer func() {
if e != nil {
result = movieCopy
}
}()
movie.Title = title
serialized, e := json.Marshal(movie)
if e != nil {
return
}
log.Printf("Serialized: %s", serialized)
return
}
Alexey is correct, but if the Movie
struct has pointer or slice fields, they wouldn't be copied.
Here's an example that manually copies the slice field (Tags
) before every update, and also features a nice transaction method (update
) I think you could use:
type Movie struct {
id int
Title string
Year int
Tags []string
}
func (m *Movie) update(fn func(m *Movie) error) error {
// Make a copy of Movie.
movieCopy := *m
// Manually copy slice and pointer fields.
movieCopy.Tags = make([]string, 0, len(m.Tags))
copy(movieCopy.Tags, m.Tags)
// Run the update transaction on the copy.
if err := fn(&movieCopy); err != nil {
return err
}
// Save to db.
data, err := json.Marshal(movieCopy)
if err != nil {
return err
}
return db.SetValue(m.id, data)
}
func (m *Movie) SetTitle(title string) error {
m.update(func(mm *Movie) error {
mm.Title = title
return nil
})
}
func (m *Movie) SetYear(year int) error {
m.update(func(mm *Movie) error {
mm.Year = year
return nil
})
}