关于golang:如何提高代码的可读性-学习笔记

64次阅读

共计 13325 个字符,预计需要花费 34 分钟才能阅读完成。

本文整顿自 taowen 徒弟在滴滴外部的分享。

1.Why

对一线开发人员来说,每天工作内容大多是在已有我的项目的根底上持续堆代码。当我的项目切实堆不动时就须要寻找收益来重构代码。既然咱们的大多数工夫都花在坐在显示器前读写代码这件事上,那可读性不好的代码都是在谋杀本人 or 共事的生命,所以不如一开始就提炼技巧,致力写好代码; )

2.How

为进步代码可读性,先来剖析代码理论运行环境。代码理论运行于两个中央:cpu 人脑。对于 cpu,代码优化需了解其工作机制,写代码时为针对 cpu 个性进行优化;对于人脑,咱们在读代码时,它像解释器一样,一行一行运行代码,从这个角度来说,要进步代码的可读性首先须要晓得大脑的运行机制。

上面来看一下人脑适宜做的事件和不适宜做的事件:

大脑善于做的事件

名称 图片 阐明
对象辨认 不同于机器学习看无数张猫片之后可能还是不能精确辨认猫这个对象,人脑在看过几只猫之后就能够很好的辨认。
空间合成 人脑不须要标注,能够直观感触到空间中的不同物体。
时序预测 你的第一感觉是不是这个哥们要被车撞了?
时序记忆 作为人类生存本能之一,咱们屡次走过某个中央时,人脑会对这个中央造成记忆。
类比揣测 人脑还有类比性能,比如说这道题大多数人会抉择 C 吧。

大脑不善于做的事件

名称 图片 例子
无奈映射到现实生活教训的抽象概念 人脑看到左图时,会比拟轻松想到通关形式,然而如果换成右图这种形象的概念,外面的对象换成了嘿嘿的像素,咱们就不晓得这是什么鬼了。比如说代码里如果充斥着 Z,X,C,V 这样的变量名,你可能就看懵了。
简短的侦探推理 这种须要递归 (or 循环) 去查看所有可能性最初找到解法的场景,人脑同样不善于。
跟踪多个同时变动的过程 大脑是个单线程的 CPU,不善于左手画圆,右手画圈。

代码优化实践

理解人脑的优缺点后,写代码时就能够依据人脑的特点对应改善代码的可读性了。这里提取出三种实践:

  1. Align Models,匹配模型:代码中的数据和算法模型 应和人脑中的 心智模型对应
  2. Shorten Process,简短解决:写代码时应 缩短“福尔摩斯探案集”的流程长度,即不要写大段代码
  3. Isolate Process,隔离解决:写代码一个流程一个流程来解决,不要同时形容多个流程的演进过程

上面通过例子具体解释这三种模型:

Align Models

在代码中,模型无外乎就是 数据结构 算法 ,而在人脑中,对应的是 心智模型,所谓心智模型就是人脑对于一个物体 or 一件事件的想法,咱们平时谈话就是心智模型的外在体现。写代码时应把代码中的名词与事实名词对应起来,缩小人脑从需要文档到代码的映射老本。比方对于“银行账户”这个名词,很多变量名都能够体现这个词,比方:bankAccount、bank_account、account、BankAccount、BA、bank_acc、item、row、record、model,编码中应对立应用和事实对象能链接上的变量名。

代码命名技巧

起变量名时候取其理论含意,没必要轻易写个变量名而后在正文外面偷偷用功。

// bad
var d int // elapsed time in days

// good
var elapsedTimeInDays int // 全局应用

起函数名 动词 + 名词联合,还要留神标识出你的自定义变量类型:

// bad
func getThem(theList [][]int) [][]int {var list1 [][]int // list1 是啥,不晓得
    for _, x := range theList {if x[0] == 4 { // 4 是啥,不晓得
            list1 = append(list1, x)
        }
    }
    return list1
}

// good
type Cell []int // 标识[]int 作用

func (cell Cell) isFlagged() bool { // 阐明 4 的作用
    return cell[0] == 4
}

func getFlaggedCells(gameBoard []Cell) []Cell { // 起有意义的变量名
    var flaggedCells []Cell
    for _, cell := range gameBoard {if cell.isFlagged() {flaggedCells = append(flaggedCells, cell)
        }
    }
    return flaggedCells
}
代码合成技巧

依照空间合成(Spatial Decomposition):上面这块代码都是与 Page 相干的逻辑,仔细观察能够依据 page 的空间合成代码:

// bad
// …then…and then … and then ... // 平淡无奇形容整个过程
func RenderPage(request *http.Request) map[string]interface{} {page := map[string]interface{}{}
    name := request.Form.Get("name")
    page["name"] = name
    urlPathName := strings.ToLower(name)
    urlPathName = regexp.MustCompile(`['.]`).ReplaceAllString(urlPathName, "")
    urlPathName = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(urlPathName, "-")
    urlPathName = strings.Trim(urlPathName, "-")
    page["url"] = "/biz/" + urlPathName
    page["date_created"] = time.Now().In(time.UTC)
    return page
}
// good
// 按空间合成,这样的益处是能够集中精力到关注的性能上
var page = map[string]pageItem{
    "name":         pageName,
    "url":          pageUrl,
    "date_created": pageDateCreated,
}

type pageItem func(*http.Request) interface{}

func pageName(request *http.Request) interface{} { // name 相干过程
    return request.Form.Get("name")
}

func pageUrl(request *http.Request) interface{} { // URL 相干过程
    name := request.Form.Get("name")
    urlPathName := strings.ToLower(name)
    urlPathName = regexp.MustCompile(`['.]`).ReplaceAllString(urlPathName, "")
    urlPathName = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(urlPathName, "-")
    urlPathName = strings.Trim(urlPathName, "-")
    return "/biz/" + urlPathName
}

func pageDateCreated(request *http.Request) interface{} { // Date 相干过程
    return time.Now().In(time.UTC)
}

依照工夫合成(Temporal Decomposition):上面这块代码把整个流程的算账和打印账单混写在一起,能够依照工夫程序对齐进行合成:

// bad 
func (customer *Customer) statement() string {totalAmount := float64(0)
    frequentRenterPoints := 0
    result := "Rental Record for" + customer.Name + "\n"

    for _, rental := range customer.rentals {thisAmount := float64(0)
        switch rental.PriceCode {
        case REGULAR:
            thisAmount += 2
        case New_RELEASE:
            thisAmount += rental.rent * 2
        case CHILDREN:
            thisAmount += 1.5
        }
        frequentRenterPoints += 1
        totalAmount += thisAmount
    }
    result += strconv.FormatFloat(totalAmount,'g',10,64) + "\n"
    result += strconv.Itoa(frequentRenterPoints)

    return result
}
// good 逻辑合成后的代码
func statement(custom *Customer) string {bill := calcBill(custom)

    statement := bill.print()

    return statement
}

type RentalBill struct {
    rental Rental
    amount float64
}

type Bill struct {
    customer             *Customer
    rentals              []RentalBill
    totalAmount          float64
    frequentRenterPoints int
}

func calcBill(customer *Customer) Bill {bill := Bill{}
    for _, rental := range customer.rentals {
        rentalBill := RentalBill{
            rental: rental,
            amount: calcAmount(rental),
        }
        bill.frequentRenterPoints += calcFrequentRenterPoints(rental)
        bill.totalAmount += rentalBill.amount
        bill.rentals = append(bill.rentals, rentalBill)
    }
    return bill
}

func (bill Bill) print() string {

    result := "Rental Record for" + bill.customer.name + "(n"

    for _, rental := range bill.rentals{
        result += "\t" + rental.movie.title + "\t" +
            strconv.FormatFloat(rental.amount, 'g', 10, 64) + "\n"
    }
    

    result += "Amount owed is" +
        strconv.FormatFloat(bill.totalAmount, 'g', 10, 64) + "\n"

    result += "You earned +" +
        strconv.Itoa(bill.frequentRenterPoints) + "frequent renter points"

    return result
}

func calcAmount(rental Rental) float64 {thisAmount := float64(0)
    switch rental.movie.priceCode {
    case REGULAR:
        thisAmount += 2
        if rental.daysRented > 2 {thisAmount += (float64(rental.daysRented) - 2) * 1.5
        }
    case NEW_RELEASE:
        thisAmount += float64(rental.daysRented) * 3
    case CHILDRENS:
        thisAmount += 1.5
        if rental.daysRented > 3 {thisAmount += (float64(rental.daysRented) - 3) * 1.5
        }
    }
    return thisAmount
}

func calcFrequentRenterPoints(rental Rental) int {
    frequentRenterPoints := 1
    switch rental.movie.priceCode {
    case NEW_RELEASE:
        if rental.daysRented > 1 {frequentRenterPointst++}
    }
    return frequentRenterPoints
}

按层合成(Layer Decomposition):

// bad
func findSphericalClosest(lat float64, lng float64, locations []Location) *Location {
    var closest *Location
  closestDistance := math.MaxFloat64
  for _, location := range locations {latRad := radians(lat)
    lngRad := radians(lng)
    lng2Rad := radians(location.Lat)
    lng2Rad := radians(location.Lng)
    var dist = math.Acos(math.Sin(latRad) * math.Sin(lat2Rad) +  
                         math.Cos(latRad) * math.Cos(lat2Rad) *
                         math.Cos(lng2Rad - lngRad) 
                        )
    if dist < closestDistance {
            closest = &location
      closestDistance = dist
    }
  }
    return closet
}
// good
type Location struct {
}

type compare func(left Location, right Location) int

func min(objects []Location, compare compare) *Location {
    var min *Location
    for _, object := range objects {
        if min == nil {
            min = &object
            continue
        }
        if compare(object, *min) < 0 {min = &object}
    }
    return min
}

func findSphericalClosest(lat float64, lng float64, locations []Location) *Location {isCloser := func(left Location, right Location) int {leftDistance := rand.Int()
        rightDistance := rand.Int()
        if leftDistance < rightDistance {return -1} else {return 0}
    }
    closet := min(locations, isCloser)
    return closet
}
正文

正文不应反复代码的工作。应该去解释代码的模型和心智模型的映射关系,应阐明为什么要应用这个代码模型,上面的例子就是反面教材:

// bad
/** the name. */
var name string
/** the version. */
var Version string
/** the info. */
var info string

// Find the Node in the given subtree, with the given name, using the given depth.
func FindNodeInSubtree(subTree *Node, name string, depth *int) *Node {}

上面的例子是侧面教材:

// Impose a reasonable limit - no human can read that much anyway
const MAX_RSS_SUBSCRIPTIONS = 1000

// Runtime is O(number_tags * average_tag_depth), 
// so watch out for badly nested inputs.
func FixBrokenHTML(HTML string) string {// ...}

Shorten Process

Shorten Process 的意思是要缩短人脑“编译代码”的流程。应该防止写出像小白鼠走迷路一样又长又绕的代码。所谓又长又绕的代码体现在,跨表达式跟踪、跨多行函数跟踪、跨多个成员函数跟踪、跨多个文件跟踪、跨多个编译单元跟踪,甚至是跨多个代码仓库跟踪。

对应的伎俩能够有:引入变量、拆分函数、提前返回、放大变量作用域,这些办法最终想达到的目标都是让大脑喘口气,不要一口气跟踪太久。同样来看一些具体的例子:

例子

上面的代码,多种复合条件组合在一起,你看了半天绕晕了可能也没看出到底什么状况下为 true,什么状况为 false。

// bad
func (rng *Range) overlapsWith(other *Range) bool {return (rng.begin >= other.begin && rng.begin < other.end) ||
        (rng.end > other.begin && rng.end <= other.end) ||
        (rng.begin <= other.begin && rng.end >= other.end)
}

然而把状况进行拆解,每种条件进行独自解决。这样逻辑就很清晰了。

// good
func (rng *Range) overlapsWith(other *Range) bool {
    if other.end < rng.begin {return false // they end before we begin}    
    if other.begin >= rng.end {return false // they begin after we end}
  return true // Only possibility left: they overlap
}

再来看一个例子,一开始你写代码的时候,可能只有一个 if … else…,起初 PM 让加一下权限管制,于是你能够开心的在 if 里持续套一层 if,补丁打完,开心出工,于是代码看起来像这样:

// bad 多层缩进的问题
func handleResult(reply *Reply, userResult int, permissionResult int) {
  if userResult == SUCCESS {
    if permissionResult != SUCCESS {reply.WriteErrors("error reading permissions")
        reply.Done()
        return
    }
    reply.WriteErrors("")
  } else {reply.WriteErrors("User Result")
  }
  reply.Done()}

这种代码也比拟好改,个别反向写 if 条件返回判否逻辑即可:

// good
func handleResult(reply *Reply, userResult int, permissionResult int) {defer reply.Done()
  if userResult != SUCCESS {reply.WriteErrors("User Result")
    return 
  }
  if permissionResult != SUCCESS {reply.WriteErrors("error reading permissions")
    return
  }
  reply.WriteErrors("")
}

这个例子的代码问题比拟费解,它的问题是所有内容都放在了 MooDriver 这个对象中。

// bad
type MooDriver struct {
    gradient Gradient
  splines []Spline}
func (driver *MooDriver) drive(reason string) {driver.saturateGradient()
  driver.reticulateSplines()
  driver.diveForMoog(reason)
}

比拟好的办法是尽可能减少全局 scope,而是应用上下文变量进行传递。

// good 
type ExplicitDriver struct { }

// 应用上下文传递
func (driver *MooDriver) drive(reason string) {gradient := driver.saturateGradient()
  splines := driver.reticulateSplines(gradient)
  driver.diveForMoog(splines, reason)
}

Isolate Process

人脑缺点是不善于同时跟踪多件事件,如果”同时跟踪“事物的多个变动过程,这不合乎人脑的结构;然而如果把逻辑放在很多中央,这对大脑也不敌对,因为大脑须要”七拼八凑“能力把一块逻辑看全。所以就有了一句很经典的废话,每个学计算机的大学生都听过。你的代码要做到 高内聚,低耦合,这样就牛逼了!-_-|||,然而你要问说这话的人什么叫高内聚,低耦合呢,他可能就得推敲推敲了,上面来通过一些例子来推敲一下。

首先先来玄学局部,如果你的代码写成上面这样,可读性就不会很高。

个别状况下,咱们能够依据业务场景致力把代码批改成这样:

举几个例子,上面这段代码十分常见,外面 version 的含意是用户端上不同的版本须要做不同的逻辑解决。

func (query *Query) doQuery() {
  if query.sdQuery != nil {query.sdQuery.clearResultSet()
  }
  // version 5.2 control
  if query.sd52 {query.sdQuery = sdLoginSession.createQuery(SDQuery.OPEN_FOR_QUERY)
  } else {query.sdQuery = sdSession.createQuery(SDQuery.OPEN_FOR_QUERY)
  }
  query.executeQuery()}

这段代码的问题是因为 版本差别 多块代码流程逻辑 Merge 在了一起,造成逻辑两头有分叉景象。解决起来也很简略,封装一个 adapter,把版本逻辑抽出一个 interface,而后依据版本实现具体的逻辑。

再来看个例子,上面代码中依据 expiry 和 maturity 这样的 产品逻辑不同 也会造成分叉景象,所以你的代码会写成这样:

// bad
type Loan struct {
    start    time.Time
    expiry   *time.Time
    maturity *time.Time
    rating   int
}

func (loan *Loan) duration() float64 {
    if loan.expiry == nil {return float64(loan.maturity.Unix()-loan.start.Unix()) / 365 * 24 * float64(time.Hour)
    } else if loan.maturity == nil {return float64(loan.expiry.Unix()-loan.start.Unix()) / 365 * 24 * float64(time.Hour)
    }
    toExpiry := float64(loan.expiry.Unix() - loan.start.Unix())
    fromExpiryToMaturity := float64(loan.maturity.Unix() - loan.expiry.Unix())
    revolverDuration := toExpiry / 365 * 24 * float64(time.Hour)
    termDuration := fromExpiryToMaturity / 365 * 24 * float64(time.Hour)
    return revolverDuration + termDuration
}

func (loan *Loan) unusedPercentage() float64 {
    if loan.expiry != nil && loan.maturity != nil {
        if loan.rating > 4 {return 0.95} else {return 0.50}
    } else if loan.maturity != nil {return 1} else if loan.expiry != nil {
        if loan.rating > 4 {return 0.75} else {return 0.25}
    }
    panic("invalid loan")
}

解决多种产品逻辑的最佳实际是 Strategy pattern,代码入下图,依据产品类型创立出不同的策略接口,而后别离实现 duration 和 unusedPercentage 这两个办法即可。

// good
type LoanApplication struct {
    expiry   *time.Time
    maturity *time.Time
}

type CapitalStrategy interface {duration() float64
    unusedPercentage() float64}

func createLoanStrategy(loanApplication LoanApplication) CapitalStrategy {
    if loanApplication.expiry != nil && loanApplication.maturity != nil {return createRCTL(loanApplication)
    }
    if loanApplication.expiry != nil {return createRevolver(loanApplication)
    }
    if loanApplication.maturity != nil {return createTermLoan}
    panic("invalid loan application")
}

然而现实情况没有这么简略,因为不同事物在你眼中就是多过程多线程运行的,比方下面产品逻辑的例子,尽管通过一些设计模式把执行的逻辑隔离到了不同中央,然而代码中只有含有多种产品,代码在执行时还是会有一个产品抉择的过程。逻辑产生在同一时间、同一空间,所以“自然而然”就须要写在了一起:

  • 性能展现时,因为须要展现多种信息,会造成 concurrent process
  • 写代码时,业务包含功能性和非功能性需要,也包含失常逻辑和异样逻辑解决
  • 思考运行效率时,为提高效率咱们会思考异步 I /O、多线程 / 协程
  • 思考流程复用时,因为版本差别和产品策略也会造成 merged concurrent process

对于多种性能杂糅在一起,比方下面的 RenderPage 函数,对应解法为不要把所有事件合在一起搞,把单块性能内聚,整体再耦合成为一个单元。

对于多个同步进行的 I / O 操作,能够通过协程把揉在一起的过程离开来:

// bad 两个 I / O 写到一起了
func sendToPlatforms() {httpSend("bloomberg", func(err error) {
        if err == nil {increaseCounter("bloomberg_sent", func(err error) {
                if err != nil {log("failed to record counter", err)
                }
            })
        } else {log("failed to send to bloom berg", err)
        }
    })
    ftpSend("reuters", func(err error) {
        if err == DIRECTORY_NOT_FOUND {httpSend("reuterHelp", err)
        }
    })
}

对于这种并发的 I / O 场景,最佳解法就是给每个性能各自写一个计算函数,代码真正运行的时候是”同时“在运行,然而代码中是离开的。

//good 协程写法
func sendToPlatforms() {go sendToBloomberg()
    go sendToReuters()}

func sendToBloomberg() {err := httpSend("bloomberg")
    if err != nil {log("failed to send to bloom berg", err)
        return
    }
    err := increaseCounter("bloomberg_sent")
    if err != nil {log("failed to record counter", err)
    }
}

func sendToReuters() {err := ftpSend("reuters")
    if err == nil {httpSend("reutersHelp", err)
    }
}

有时,逻辑必须要合并到一个 Process 外面,比方在交易商品时必须要对参数做逻辑查看:

// bad
func buyProduct(req *http.Request) error {err := checkAuth(req)
    if err != nil {return err}
    // ...
}

func sellProduct(req *http.Request) error {err := checkAuth(req)
    if err != nil {return err}
    // ...
}

这种头部有公共逻辑经典解法是写个 Decorator 独自解决权限校验逻辑,而后 wrapper 一下正式逻辑即可:

// good 装璜器写法
func init() {buyProduct = checkAuthDecorator(buyProduct)
    sellProduct = checkAuthDecorator(sellProduct)
}

func checkAuthDecorator(f func(req *http.Request) error) func(req *http.Request) error {return func(req *http.Request) error {err := checkAuth(req)
        if err != nil {return err}
        return f(req)
    }
}

var buyProduct = func(req *http.Request) error {// ...}

var sellProduct = func(req *http.Request) error {// ...}

此时你的代码会想这样:

当然公共逻辑不仅仅存在于头部,认真思考一下所谓的 strategy、Template pattern,他们是在逻辑的其余中央去做这样的逻辑解决。

这块有一个新的概念叫:信噪比 。信噪比是一个绝对概念,信息,对有用的;乐音,对 没用的。代码应把什么逻辑写在一起,不仅取决于读者是谁,还取决于这个读者过后心愿实现什么指标。

比方上面这段 C ++ 和 Python 代码:

void sendMessage(const Message &msg) const {...}
![image.png](/img/bVcOMhy)
def sendMessage(msg):

如果你当初要做业务开发,你可能会感觉 Python 代码读起来很简洁;然而如果你当初要做一些性能优化的工作,C++ 代码显然能给你带来更多信息。

再比方上面这段代码,从业务逻辑上讲,这段开发看起来十分清晰,就是去遍历书本获取 Publisher。

for _, book := range books {book.getPublisher()
}

然而如果你看了线上打了如下的 SQL 日志,你懵逼了,心想这个 OOM 真 **,真就是一行一行执行 SQL,这行代码可能会引起 DB 报警,让你的 DBA 共事中午起来修 DB。

SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id

所以如果代码改成这样,你可能就会更加明确这块代码其实是在循环调用实体。

for _, book := range books {loadEntity("publisher", book.publisher_id)
}

总结一下:

  • 优先尝试给每 个 Process 一个本人的函数,不要合并到一起来算

    • 尝试界面拆成组件
    • 尝试把订单拆成多个单据,独立跟踪多个流程
    • 尝试用协程而不是回调来表白 concurrent i/o
  • 如果不得不在一个 Process 中解决多个绝对独立的事件

    • 尝试复制一份代码,而不是复用同一个 Process
    • 尝试显式插入: state/ adapter/ strategy/template/ visitor/ observer
    • 尝试隐式插入: decorator/aop
    • 进步信噪比是绝对于具体指标的,进步了一个指标的信噪比,就升高了另外一个指标的信噪比

3. 总结

当咱们吐槽这块代码可读性太差时,不要把可读性差的起因简略归结为正文不够 或者不 OO,而是能够从人脑个性登程,依据上面的图片去找到代码问题,而后试着改良它(跑了几年的老代码还是算了,别改一行线上全炸了:)

正文完
 0