樱桃红了

北上念书,对帝都的古迹早已没有了兴趣,对其自然风光更是嗤之以鼻,哪怕是一年中最好的时节也是扬尘满天,鸡毛柳絮,实在毫无踏青的兴致。

在帝都的七年里,跟大多数不谙世事、人生懵懂的大学生一样,耗着最好的年华,度着乏善可陈的无聊。只是,一直放不下的是南国的春天,尤其是院子外的那两颗樱桃树。

求学的那几年,平日的活动范围很难超出学校方圆十里。即使是难得的春光,也是经常是躲在实验室磨着看似忙碌的无聊。心里唯一念念不忘的是这个时节的一味水果——樱桃。

那时也还算没有到囊中羞涩到买不起水果。但是却一直没有在帝都寻觅到真正的樱桃。路边倒是有很多小贩叫卖着一种叫车厘子的大樱桃。虽然价格似乎比樱桃贵上不少,但是在鄙人心中,那是一味除了能看,绝非能食的水果外,别无其他什么特点。况且,车厘子其实是可以从智利通过轮渡远洋运输的,并不算什么当季难得的水果。

帝都七年,没有尝过一颗南国的樱桃。

学未有所成,惭愧归来。生活所迫逐渐多过了理所当然的无知与轻率。喜欢周末网老家跑,春天更是如此。搬出一个方凳,对这院子外的樱桃树坐着,晒着太阳发呆。

按我们当地种地人的标准,我们家应该都是不合格的农人。因此,院子里的樱桃树并非亲自所植。而是在自己约摸快上小学的时候,爷爷的兄弟,我的幺叔公为自己移栽的。印象中,幺叔公比爷爷要高大,但是身体没有爷爷硬朗,平日里有点轻微的喘。而在栽下这棵树以前,每到春天,我心心念念的就是幺叔公家后院的那颗樱桃树。那颗树非常高,基本覆盖了那一块空地。由于位置隐蔽,又背靠一条灌水渠,那棵树几乎从来没有让自己失望过。这是印象中最好吃的樱桃的锚点:颗颗肥硕饱满,色泽红润,深邃而通透,入口前味清甜,中后味醇厚悠长。别说从树上掉下来,就是摘的时候忘记了轻重也可能把樱桃捏坏了。

院子前的樱桃树栽好的时候,幺叔公跟我说,过两年就可以吃樱桃啦。鄙人高兴得不知所错。过了一会,问道,一定要过两年吗?两年好长呀,一年可以吗?幺叔公只是一直笑,没有回答我。我并没有放弃寻找这个问题的答案。加之幺叔公的制作竹子相关农具的手艺很好,我更是每天一边观赏其干活,一边重复问这个答案。孩提时代的两年也许真的不短,但是,一辈子有真的长吗?

那个时候,幺叔公跟我说得最多的一句话是,如果他年老吃不动了,生活也不能自理了,来到我家的时候,我还会善待他吗?我小手一挥,毫不犹豫的说,那还用说,我加门前的两棵樱桃树就是左右护法。

北上以后,没有了这最好的一味水果相伴,记忆却更加深刻。

一日,跟妈通电话。老妈非常遗憾的说起幺叔公进来身体每况愈下。唯一安心一点的是,在这件事上,爸妈真的是尽心尽力,医院、手续以及农村固有的陋习带来的阻力都是他们对待至亲一般的落实、解决。那年夏天,暑假回来。每次去爷爷奶奶家,都会顺带路过幺叔公家。北上前,跟父母要了一点零花钱,仗着并不熟悉驾驶经验,驱车去县里买了一些伴手礼,聊表心意。那也是最后一次见到幺叔公,虽然卧病在床,但是精神很好,思维也非常清晰。依然忘不了用他年老我是否还能如此孝顺来洗刷我,我结婚的时候会不会邀请他最八仙桌的上桌……那年的我只是笑,虽然那时已经有了若,但是却不知道该不该告诉他。

岁月无声有痕。门前的两棵殷桃树如今也亭亭如盖,幺叔公坟头周围也长满了茱萸草。挂果时节,我依然喜欢爬上树去吃。坦率讲,我们家栽树就没有好手。所以,这两棵树的樱桃在鄙人如此耐受酸味的前提下也是很酸的。只是,偶尔能觅到一颗不是那么酸的果子的时候,让人一下又找回了幺叔公后院那棵树上的感觉。

北上求学那几年,无法亲自采摘。有那么一两回,是老爸把若接回老家提我完成这件事。虽然我知道若其实并不知道我的心结,她更多是帮我扮演吃货的角色,但是,好歹她总能拍出几张应景入心的照片发与我看。这是当初的若,也是如今的若,俏皮、善良、漂亮。想起幺叔公当年的调侃,看看若,仿佛这还算是件骄傲的事呢。

回成都以后,若的好友经常邀去简阳樱桃沟采摘。对于这件事,我是毫无抵抗力的。这几年来,几乎从未缺席。他们都笑我每次去只知道吃樱桃,我也不装得像个巨婴一样,因为,对于我来说,这一味水果是真的稀罕。

又是一年,樱桃红了。依然稀罕,粒粒珍惜。

正则表达式中匹配 Unicode 的常用类别和命名块

大概两年前,在Golang正则表达式使用及简单示例中提到了在正则表达式中使用\p{Lu}来匹配Unicode 类别或 Unicode 块:

但是,在日常使用的时候经常不知道自己要匹配的那个 Unicode 字符属于拿一个类别。于是翻了一下 Golang 所遵循的 RE2 列别表。把一些常用的类别和命名块列举如下:

常用 Unicode 常规类别

类别 描述
Ll 小写字母
Lu 大写字母
Lt 首字母大写字母
Lo 其他字母(注音字母、表意文字等)
L 字母,== Lu | Ll | Lt | Lm | Lo
Sm 数学符号
Sc 货币符号

比较遗憾的是,目前还没有专门的 Emoji 类别。因此,目前如果你想匹配 Emoji 表情的话,还是需要写 Unicode 范围表达式,诸如 [\x{1F600}-\x{1F6FF}|[\x{2600}-\x{26FF}],来匹配表情符号。

常用 Unicode 命名块

名称 描述
Greek 希腊语
Han 汉语
Tibetan 藏语
Thai 泰语
Latin 拉丁语
Hebrew 希伯来语

扩展阅读

Golang strings.Builder 原理解析

背景

在很多场景中,我们都会进行字符串拼接操作。

最开始的时候,你可能会使用如下的操作:

package main

func main() {
    ss := []string{
        "A",
        "B",
        "C",
    }

    var str string
    for _, s := range ss {
        str += s
    }

    print(str)
}

与许多支持string类型的语言一样,golang中的string类型也是只读且不可变的。因此,这种拼接字符串的方式会导致大量的string创建、销毁和内存分配。如果你拼接的字符串比较多的话,这显然不是一个正确的姿势。

在 Golang 1.10 以前,你可以使用bytes.Buffer来优化:

package main

import (
    "bytes"
    "fmt"
)

func main() {
    ss := []string{
        "A",
        "B",
        "C",
    }

    var b bytes.Buffer
    for _, s := range ss {
        fmt.Fprint(&b, s)
    }

    print(b.String())
}

这里使用 var b bytes.Buffer 存放最终拼接好的字符串,一定程度上避免上面 str 每进行一次拼接操作就重新申请新的内存空间存放中间字符串的问题。

但这里依然有一个小问题: b.String() 会有一次 []byte -> string 类型转换。而这个操作是会进行一次内存分配和内容拷贝的。

使用 strings.Builder 进行字符串拼接

如果你现在已经在使用 golang 1.10, 那么你还有一个更好的选择:strings.Builder:

package main

import (
    "fmt"
    "strings"
)

func main() {
    ss := []string{
        "A",
        "B",
        "C",
    }

    var b strings.Builder
    for _, s := range ss {
        fmt.Fprint(&b, s)
    }

    print(b.String())
}

Golang官方将strings.Builder作为一个feature引入,想必是有两把刷子。不信跑个分?简单来了个benchmark:

package ts

import (
    "bytes"
    "fmt"
    "strings"
    "testing"
)

func BenchmarkBuffer(b *testing.B) {
    var buf bytes.Buffer
    for i := 0; i < b.N; i++ {
        fmt.Fprint(&buf, "?")
        _ = buf.String()
    }
}

func BenchmarkBuilder(b *testing.B) {
    var builder strings.Builder
    for i := 0; i < b.N; i++ {
        fmt.Fprint(&builder, "?")
        _ = builder.String()
    }
}
╰─➤  go test -bench=. -benchmem                                                                                                                         2 ↵
goos: darwin
goarch: amd64
pkg: test/ts
BenchmarkBuffer-4         300000        101086 ns/op      604155 B/op          1 allocs/op
BenchmarkBuilder-4      20000000            90.4 ns/op        21 B/op          0 allocs/op
PASS
ok      test/ts 32.308s

性能提升感人。要知道诸如C#, Java这些自带GC的语言很早就引入了string builder, Golang 在1.10才引入,时机其实不算早,但是巨大的提升终归没让人失望。下面我们看一下标准库是如何做到的。

strings.Builder 原理解析

strings.Builder的实现在文件strings/builder.go中,一共只有120行,非常精炼。关键代码摘选如下:

type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte // 1
}

// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
    b.copyCheck()
    b.buf = append(b.buf, p...) // 2
    return len(p), nil
}

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))  // 3
}

func (b *Builder) copyCheck() {
    if b.addr == nil {
        // 4
        // This hack works around a failing of Go's escape analysis
        // that was causing b to escape and be heap allocated.
        // See issue 23382.
        // TODO: once issue 7921 is fixed, this should be reverted to
        // just "b.addr = b".
        b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
    } else if b.addr != b {
        panic("strings: illegal use of non-zero Builder copied by value")
    }
}

  1. byte.Buffer思路类似,既然 string 在构建过程中会不断的被销毁重建,那么就尽量避免这个问题,底层使用一个 buf []byte 来存放字符串的内容。
  2. 对于写操作,就是简单的将byte写入到 buf 即可。
  3. 为了解决bytes.Buffer.String()存在的[]byte -> string类型转换和内存拷贝问题,这里使用了一个unsafe.Pointer的存指针转换操作,实现了直接将buf []byte转换为 string类型,同时避免了内存充分配的问题。
  4. 如果我们自己来实现strings.Builder, 大部分情况下我们完成前3步就觉得大功告成了。但是标准库做得要更近一步。我们知道Golang的堆栈在大部分情况下是不需要开发者关注的,如果能够在栈上完成的工作逃逸到了堆上,性能就大打折扣了。因此,copyCheck 加入了一行比较hack的代码来避免buf逃逸到堆上。关于这部分内容,你可以进一步阅读Dave Cheney的关于Go’s hidden #pragmas.

就此止步?

一般Golang标准库中使用的方式都是会逐步被推广,成为某些场景下的最佳实践方式。

这里使用到的*(*string)(unsafe.Pointer(&b.buf))其实也可以在其他的场景下使用。比如:如何比较string[]byte是否相等而不进行内存分配呢??似乎铺垫写得太明显了,大家应该都会写了,直接给出代码吧:

func unsafeEqual(a string, b []byte) bool {
    bbp := *(*string)(unsafe.Pointer(&b))
    return a == bbp
}

扩展阅读

The State of Go 1.10