英文:
Designing Go packages: when I should define methods on types?
问题
假设我有一个类型 type T int
,我想定义一个在这个类型上操作的逻辑。
我应该使用什么抽象和何时使用?
-
在该类型上定义一个方法:
func (T t) someLogic() { // ... }
-
定义一个函数:
func somelogic(T t) { // ... }
英文:
Suppose that I have a type type T int
and I want to define a logic to operate on this type.
What abstraction should I use and When ?
-
Defining a method on that type:
func (T t) someLogic() { // ... }
-
Defining a function:
func somelogic(T t) { // ... }
答案1
得分: 5
一些使用方法的情况:
- **修改接收器:**通常会使用方法来修改对象的字段。对于用户来说,
x.Foo
修改X的字段比Foo(x)
修改X更容易理解。 - **通过接收器产生副作用:**如果某些操作以更微妙的方式对对象产生副作用,比如向
struct
中的网络连接写入数据,或者通过指针、切片等方式写入数据,通常会将其定义为类型的方法。 - **访问私有字段:**理论上,同一个包中的任何内容都可以访问对象的非导出字段,但通常只有对象的构造函数和方法可以访问。让其他内容访问非导出字段有点像在C++中使用
friend
。 - **满足接口的需要:**只有方法可以成为接口的一部分,因此为了满足接口的要求,可能需要将某些内容定义为方法。例如,Peter Bourgon的Go入门指南将
type openWeatherMap
定义为一个空的结构体,并添加了一个方法,而不是一个函数,只是为了满足与其他实现不是空结构体的相同的weatherProvider
接口。- **测试桩:**作为上述情况的特例,有时接口有助于为测试创建桩对象,因此即使没有状态,你的桩实现可能也必须是方法。
一些使用函数的情况:
- 构造函数:
func NewFoo(...) (*Foo)
是一个函数,而不是方法。Go语言没有构造函数的概念,所以只能这样定义。 - **在接口或基本类型上运行:**无法在
interface
或基本类型上添加方法(除非使用type
将其定义为新类型)。因此,strings.Split
和reflect.DeepEqual
必须是函数。此外,io.Copy
必须是一个函数,因为它不能在Reader
或Writer
上定义一个方法。请注意,这些函数并没有声明一个新类型(例如strings.MyString
)来解决无法在基本类型上进行方法定义的问题。 - **将功能从过大的类型或包中移出:**有时一个单一的类型(比如某些Web应用中的
User
或Page
)会积累大量功能,这会影响可读性、组织性,甚至会导致结构问题(比如如果更难避免循环导入)。将一个不会修改接收器、访问非导出字段等的方法转换为非方法,可能是将其代码“上移”到应用程序的更高层或“转移到”另一个类型/包的重构步骤,或者独立函数只是最自然的长期位置。(感谢Steve Francia在关于他的Go错误的演讲中包含了hugo的一个示例。) - **方便的“使用默认值”函数:**如果用户希望以快速方式使用“默认”对象值而不是显式创建对象,可以公开提供执行此操作的函数,通常与对象方法同名。例如,
http.ListenAndServe()
是一个包级函数,它创建一个简单的http.Server
并调用其ListenAndServe
方法。 - **用于传递行为的函数:**有时你不需要定义一个类型和接口来传递功能,一个简单的函数就足够了,比如
http.HandleFunc()
、template.Funcs()
或用于注册go vet
检查等。不要强迫使用。 - **如果强制使用面向对象编程:**如果你的
main()
或init()
通过调用一些辅助函数更清晰,或者你有一些不查看任何对象字段且永远不会查看的私有函数。同样,如果在你的情况下没有任何好处,不要强迫使用面向对象编程(比如type Application struct{...}
)。
如果不确定,首先编写清晰的文档,然后编写自然的代码(o.Verb()
或o.Attrib()
),然后根据感觉选择,不要过于纠结,因为通常可以稍后重新排列。
英文:
Some situations where you tend to use methods:
- Mutating the receiver: Things that modify fields of the objects are often methods. It's less surprising to your users that
x.Foo
will modify X than thatFoo(x)
will. - Side effects through the receiver: Things are often methods on a type if they have side effects on/through the object in subtler ways, like writing to a network connection that's part of the
struct
, or writing via pointers or slices or so on in the struct. - Accessing private fields: In theory, anything within the same package can see unexported fields of an object, but more commonly, just the object's constructor and methods do. Having other things look at unexported fields is sort of like having C++
friend
s. - Necessary to satisfy an interface: Only methods can be part of interfaces, so you may need to make something a method to just satisfy an interface. For example, Peter Bourgon's Go intro defines
type openWeatherMap
as an empty struct with a method, rather than a function, just to satisfy the sameweatherProvider
interface as other implementations that aren't empty structs.- Test stubbing: As a special case of the above, sometimes interfaces help stub out objects for testing, so your stub implementations might have to be methods even if they have no state.
Some where you tend to use functions:
- Constructors:
func NewFoo(...) (*Foo)
is a function, not a method. Go has no notion of a constructor, so that's how it has to be. - Running on interfaces or basic types: You can't add methods on
interface
s or basic types (unless you usetype
to make them a new type). So,strings.Split
andreflect.DeepEqual
must be functions. Also,io.Copy
has to be a function because it can't just define a method onReader
orWriter
. Note that these don't declare a new type (e.g.,strings.MyString
) to get around the inability to do methods on basic types. - Moving functionality out of oversized types or packages: Sometimes a single type (think
User
orPage
in some Web apps) accumulates a lot of functionality, and that hurts readability or organization or even causes structural problems (like if it becomes harder to avoid cyclic imports). Making a non-method out of a method that isn't mutating the receiver, accessing unexported fields, etc. might be a refactoring step towards moving its code "up" to a higher layer of the app or "over" to another type/package, or the standalone function is just the most natural long-term place for it. (Hat tip Steve Francia for including an example of this from hugo in a talk about his Go mistakes.) - Convenience "just use the defaults" functions: If your users might want a quick way to use "default" object values without explicitly creating an object, you can expose functions that do that, often with the same name as an object method. For instance,
http.ListenAndServe()
is a package-level function that makes a trivialhttp.Server
and callsListenAndServe
on it. - Functions for passing behavior around: Sometimes you don't need to define a type and interface just to pass functionality around and a bare function is sufficient, as in
http.HandleFunc()
ortemplate.Funcs()
or for registeringgo vet
checks and so on. Don't force it. - Functions if object-orientation would be forced: Say your
main()
orinit()
are cleaner if they call out to some helpers, or you have private functions that don't look at any object fields and never will. Again, don't feel like you have to force OO (à latype Application struct{...}
) if, in your situation, you don't gain anything by it.
When in doubt, if something is part of your exported API and there's a natural choice of what type to attach it to, make it a method. However, don't warp your design (pulling concerns into your type or package that could be separate) just so something can be a method. Writer
s don't WriteJSON
; it'd be hard to implement one if they did. Instead you have JSON functionality added to Writer
s via a function elsewhere, json.NewEncoder(w io.Writer)
.
If you're still unsure, first write so that the documentation reads clearly, then so that code reads naturally (o.Verb()
or o.Attrib()
), then go with what feels right without sweating over it too much, because often you can rearrange it later.
答案2
得分: 3
如果你正在操作对象的内部secrets
,请使用该方法:
func someLogic(t *T) {
t.mu.Lock()
// ...
}
如果你正在使用对象的public interface
,请使用该函数:
func somelogic(t *T) {
t.DoThis()
t.DoThat()
}
英文:
Use the method if you are manipulating internal secrets
of your object
(T *t) func someLogic() {
t.mu.Lock()
...
}
Use the function if you are using the public interface
of the object
func somelogic(T *t) {
t.DoThis()
t.DoThat()
}
答案3
得分: 0
如果你想改变 T 对象,请使用以下方式:
func (t *T) someLogic() {
// ...
}
如果你不想改变 T 对象,而是想使用原始对象的方式,请使用以下方式:
func (t T) someLogic() {
// ...
}
但是请记住,这将生成一个临时对象 T 来调用 someLogic
。
如果你喜欢 C 语言的方式,请使用以下方式:
func somelogic(t T) {
t.DoThis()
t.DoThat()
}
或者
func somelogic(t T) {
t.DoThis()
t.DoThat()
}
还有一件事,类型在 golang 中是在变量后面声明的。
英文:
if you want to change T object, use
func (t *T) someLogic() {
// ...
}
if you donn't change T object and would like a origined-object way , use
func (t T) someLogic() {
// ...
}
but remeber that this will generate a temporay object T to call someLogic
if your like the way c language does, use
func somelogic(t T) {
t.DoThis()
t.DoThat()
}
or
func somelogic(t T) {
t.DoThis()
t.DoThat()
}
one more thing , the type is behide the var in golang.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论