Golang中接口的命名规范探讨

Golang中接口的命名规范探讨 我想创建一个栈接口,然后实现它的两种方式:StackImpl 和 StackConcurrentImpl(第二种实现是并发安全的)。

关于命名有什么建议吗?我应该创建两个包,stack 和 concurrentStack,然后在每个包中定义一个 Stack 接口,并有一个实现该接口的 Impl 结构体吗?

命名接口及其实现结构体的惯用方式是什么?

5 回复

Impl 后缀绝对是 Java 风格的,在 Go 语言中并不常见。

只需定义一个栈接口,然后在普通的栈和并发栈上实现其方法即可。

更多关于Golang中接口的命名规范探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


如果一个类型的主要作用是以一种特定方式实现接口,并且这种特定方式对用户很重要,那么就将这种方式与接口名称一起包含在类型名称中。在 Java 中,接口 MapHashMapTreeMap 实现。HashTree如何实现,Map是什么

大家的回答都很棒。你关于“是什么”和“如何实现”的观点非常好——我会坚持这一点。

次要说明:我个人对名称中的缩写非常反感,除了众所周知的缩写如JSON、XML、HTML等。如今大多数(所有)IDE都会提供帮助,所以你无需完整拼写整个单词,因此我会避免使用像 StackImplStackConcurrentImpl 这样的名称,而是完整写出全名。我更喜欢清晰、描述性的名称,而不是简短的名字。

(顺便说一句,这是我不喜欢Go的一点。在我看来,标准库中充满了糟糕的缩写,比如strconv、regexp等等。)

不过,如果这对你有效的话……

官方的 Effective Go 文档中有一个关于接口命名的章节:

按照惯例,单方法接口的名称由方法名加上 -er 后缀或类似的修改构成,以形成一个代理名词:ReaderWriterFormatterCloseNotifier 等。

通常,保持接口中方法的数量尽可能少是一种良好的实践(即“SOLID”原则中的“I”)。一个方法是一个很好的数量,这将允许你遵循上述命名约定。

如果你无法将接口中的方法数量减少到一个,那么接口的名称至少应该告诉读者,实现该接口的对象可以期待什么样的行为。Stack 可能是一个好名字,因为对普通读者来说,它暗示了像 PushPop 这样的行为。

如果一个类型只是实现了一个接口,但这并不是它的主要功能,那么它的名称就不应该引用接口的名称。将 Reader 包含在每个实现 Reader 接口的类型的名称中,这是一个糟糕的想法。许多类型都没有在其名称中包含 Reader

如果一个类型的主要功能是以一种特定的方式实现一个接口,并且这种特定方式对用户来说很重要,那么就将这种方式与接口名称一起包含在类型的名称中。在 Java 中,接口 MapHashMapTreeMap 实现。HashTree如何实现,Map什么

永远不要使用 Impl,或者更糟糕的 SimpleStackBasicStack 或其他无法说明类型如何实现接口的名称。如果你无法想出一个能告诉读者实现方式的好名字,也许根本就不需要接口。

在Go语言中,接口和实现的命名遵循一些惯用约定。对于你的场景,通常建议将接口和实现放在同一个包中,并使用清晰的命名来区分它们。

接口命名

  • 接口名通常使用名词,如果接口只有一个方法,通常以"er"结尾(如ReaderWriter
  • 对于栈这种抽象数据类型,直接使用Stack作为接口名是合适的

实现命名

  • 实现结构体通常使用描述性的名称,表明其特性
  • 避免使用Impl后缀,而是使用更具描述性的名称

推荐方案

// stack/stack.go
package stack

// Stack 接口定义
type Stack interface {
    Push(item interface{})
    Pop() interface{}
    Peek() interface{}
    IsEmpty() bool
    Size() int
}

// ArrayStack 基于数组的实现
type ArrayStack struct {
    items []interface{}
}

func NewArrayStack() *ArrayStack {
    return &ArrayStack{
        items: make([]interface{}, 0),
    }
}

func (s *ArrayStack) Push(item interface{}) {
    s.items = append(s.items, item)
}

func (s *ArrayStack) Pop() interface{} {
    if s.IsEmpty() {
        return nil
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item
}

// ... 其他方法实现

// ConcurrentStack 并发安全的栈实现
type ConcurrentStack struct {
    items []interface{}
    mu    sync.RWMutex
}

func NewConcurrentStack() *ConcurrentStack {
    return &ConcurrentStack{
        items: make([]interface{}, 0),
    }
}

func (s *ConcurrentStack) Push(item interface{}) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.items = append(s.items, item)
}

func (s *ConcurrentStack) Pop() interface{} {
    s.mu.Lock()
    defer s.mu.Unlock()
    if len(s.items) == 0 {
        return nil
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item
}

// ... 其他方法实现

使用示例

package main

import (
    "fmt"
    "yourmodule/stack"
)

func main() {
    // 使用普通栈
    arrayStack := stack.NewArrayStack()
    arrayStack.Push(1)
    arrayStack.Push(2)
    fmt.Println(arrayStack.Pop()) // 2
    
    // 使用并发安全栈
    concurrentStack := stack.NewConcurrentStack()
    concurrentStack.Push("a")
    concurrentStack.Push("b")
    fmt.Println(concurrentStack.Pop()) // "b"
}

关键点

  1. 单一包结构:将相关接口和实现在同一个包中,便于管理
  2. 描述性命名ArrayStackConcurrentStack清晰地表明了实现特性
  3. 构造函数模式:使用NewXxx()函数创建实例,这是Go的惯用方式
  4. 接口在前:在包中先定义接口,再定义实现

这种设计允许用户通过接口类型使用栈,同时可以根据需要选择具体的实现:

func processStack(s stack.Stack) {
    // 可以接受任何Stack实现
    s.Push("data")
    // ...
}

不需要创建单独的concurrentStack包,因为并发安全是实现的特性,不是接口的职责。用户可以根据需要选择使用ArrayStackConcurrentStack

回到顶部