Go Getter方法与字段,适当的命名

8
在Effective Go中,明确说明,如果一个字段未导出(以小写字母开头),则getter方法应该具有相同的字段名,但以大写字母开头;他们给出的例子是一个字段owner和一个方法Owner。他们明确建议不要在getter方法名称前使用Get
我经常遇到需要将字段导出进行JSON编组、与ORM一起使用或其他反射相关目的(如果我没记错,reflect可以读取但不能修改未导出的字段)的情况,因此我的字段需要像上面的例子中那样被称为Owner,因此不能有Owner方法。
有没有一种习惯用法来解决这种情况? 编辑:以下是我遇到的具体问题的示例:
type User struct {
    Username string `db:"username" json:"username"`
    // ...
}

// code in this package needs to do json.Unmarshal(b, &user), etc.

.

// BUT, I want code in other packages to isolate themselves from
// the specifics of the User struct - they don't know how it's
// implemented, only that it has a Username field.  i.e.
package somethingelse

type User interface {
    Username() string
}

// the rest of the code in this other package works against the
// interface and is not aware of the struct directly - by design,
// because it's important that it can be changed out without
// affecting this code

1
只需使用“Get”前缀即可。这就是Go protobuf生成器的做法。 - user142162
6
如果字段被公开,为什么还要使用Getter/Setter呢?这只会让接口更加混乱,因为它允许以多种方式完成完全相同的操作。 - Jonathan Hall
3
请看编辑 - 如果您尝试通过接口隔离代码的某些部分,则这不是两种相同的方法。直接了解结构的代码使用字段,只通过接口了解它的代码使用方法。 - Brad Peabody
如果您只需要用户名,那么为什么要使用接口呢?直接传递用户名即可。 - Gavin
我正在编写一个库,其中提供了一个默认的用户;但是使用该库的开发人员需要能够使用自己的结构体(struct),因此需要一个接口来与系统的其他部分进行交互。我还有至少3-4个其他对象/结构体也适用于这种模式。 - Brad Peabody
显示剩余3条评论
2个回答

15

如果你的字段被导出,请不要使用getter和setter,这会混淆接口。

如果你需要getter或setter来执行有效性验证、格式化等操作,或者因为需要结构体满足接口,则不要导出底层字段!

如果你需要一个用于JSON、数据库访问等目的的导出字段,并且你需要getter/setter,则使用两个结构体,一个是导出的,另一个是私有的,并在公共结构体上定义自定义的JSON编组器(或db访问方法):

type jsonFoo struct {
    Owner string `json:"owner"`
}

type Foo struct {
    owner string
}

func (f *Foo) SetOwner(username string) {
    // validate/format username
    f.owner = username
}

func (f *Foo) Owner() string {
    return f.owner
}

func (f *Foo) MarshalJSON() ([]byte, error) {
    return json.Marshal(jsonFoo{
        Owner: f.owner,
    })
}

我明白你的想法,这确实可行,但我想避免这种方法。请查看我的原始帖子编辑,了解我尝试做什么的更多信息。 - Brad Peabody
1
有趣 - 你说得对,使用两个单独的结构体可能并不太糟糕。这是一种变化,可以处理编组而无需自定义代码 - 具有字段的结构体嵌入在具有方法的外部结构体中:https://play.golang.org/p/l_-PRmmydG - Brad Peabody
1
使用接口是同时使用结构体的一个好理由。但是,你仍然应该同时使用结构体,因为同时拥有getter/setter和导出字段仍然会非常混乱,而且是不良设计。 - Jonathan Hall
@squirtgun Go采用了明确而不是简洁的哲学。这是使Go易于阅读和学习的因素之一。 - Jonathan Hall
@Flimzy 我知道。但我只是表达我的观点,看看是否有其他人有同感。我觉得如果不需要这样做,阅读起来会更容易。 - squirtgun
显然有些人同意你的观点,因为他们设计了一些隐藏此类复杂性的编程语言 :) 那些强烈反对此种做法的人通常不喜欢Go。但这没关系。 - Jonathan Hall

3
我将根据@Flimzy的回答进行翻译,并发布为答案。
基本思路是创建一个具有导出字段的结构体,可用于编组,并创建另一个单独的结构体,其唯一目的是提供满足所需接口的方法。
这不需要自定义编组代码,并且在我看来给每个结构体都赋予了明确的含义。
type user struct {
    Username string `json:"username"`
}

type ExportedUser struct {
    // EDIT: explicitly doing "user *user" instead of just doing "*user"
    // helps avoid confusion between between field names and methods
    user *user
}

func (u ExportedUser) Username() string { return u.user.Username }

func main() {
    fmt.Printf("Test: %q", ExportedUser{user: &user{Username: "joe"}}.Username())
}

我有点不确定上面是应该用user还是User - 因为在Go模板中,类型可见可能是有意义的,例如{{euser.User.SomeOtherField}},这样如果需要的话,就可以访问所有字段。无论如何,上面的答案都可以按照任何一种方式运行。


这个解决方法仍然会导出嵌入的“Username”字段。尝试这个:x:= ExportedUser {user:user {Username:“joe”}}; fmt.Println(x.Username)。您可以通过在“ExportedUser”中将 * user 命名为一个字段来解决此问题,而不是嵌入它,如 type ExportedUser struct {user * user} - Jonathan Hall
@Flimzy 说得对 - 虽然我示例中的代码确实可以编译,但是让 User 只有字段,ExportedUser 只有方法,并排除任何 Go 的语法糖(其中它将 u.FieldName 翻译为 u.embeddedStruct.FieldName)可能会引起混淆,这样更清晰明了。 - Brad Peabody
1
当然,你的代码是有效的,应该可以编译。只是它没有解决通过getter/setter模式和导出变量两种方式同时暴露相同数据的问题。但我认为你理解了其中的影响,现在你可以做出决定 :) - Jonathan Hall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接