速读《effective go》

1. 介绍

2. 格式化

3. 注释

4. 命名

5. 分号

6. 控制结构

7. 函数

8. 数据

9. 初始化

10. 方法

11. 接口和其它类型

12. 空白标识符

13. 内嵌

14. 并发

15. 错误处理

1. 介绍[1]

本文档提供了编写清晰、惯用的GO代码的技巧。

2. 格式化[2]

gofmt自动生成统一风格代码格式,程序员无需关心代码格式问题。

3. 注释[3]

Go提供C风格的块注释/* */和C++风格的行注释//。一般使用行注释,包注释使用块注释。

4. 命名[4]

首字母大写的命名包外可见。

包名

包名应该简洁明了,便于记忆。建议是一个小写单词,不包含下划线和混合大小写。
import "src/encoding/base64"后,导入包用base64代替。这使得包中导出名字避免冗余,bufio.Readerbufio.BufReader相比,更简洁明了。

Getters

小写命名做为包内成员变量,大写命名做为公开成员读方法,公开成员写方法。例如,owner为包内成员变量,Owner()为公开方法返回ownerSetOwner()为公开方法修改owner变量。

接口命名

单方法接口采用方法名加er后缀方式命名,例如ReaderWrtier。除非用途和签名完全一致,不要采用ReadWriteString等系统保留方法名。转换字符串的方法名为String而非ToString

多词命名

采用MixedCaps或mixedCaps风格,而非下划线。

5. 分号[5]

大多数情况下不需要输入分号,go语法分析器会自动插入。for语句或多条语句在一行时,需要输入分号分割语句。自动插入的一个副作用是左花括号不能在一行开头。

6. 控制结构[6]

If

if可以接受一个初始语句

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

重声明和重赋值

f, err := os.Open(name)这条语句声明并赋值两个变量ferr。符合下面条件时:=可对已存在变量v重赋值。

  • 同一作用域中v已存在。(否则在不同作用域中声明新变量v
  • 右边值可以正确赋给v
  • 至少产生一个新声明变量

例如,d, err := f.Stat()
go语言中,函数参数和返回值同函数体具有相同的作用域。

For

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

针对集合对象,使用range

for key, value := range oldMap {
    newMap[key] = value
}

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

sum := 0
for _, value := range array {
    sum += value
}

go不支持++--运算,但支持多赋值

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

Switch

go的switch表达式可以不为常量甚至不为数字,依次比较每个case直到匹配。表达式为空意味匹配true

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

匹配成功后就会返回,case子句支持逗号分隔。

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

break在swtich中并不常见,但也可以和label配合,直接跳到外层。

Loop:
    for n := 0; n < len(src); n += size {
        switch {
        case src[n] < sizeOne:
            if validateOnly {
                break
            }
            size = 1
            update(src[n])

        case src[n] < sizeTwo:
            if n+1 >= len(src) {
                err = errShortInput
                break Loop
            }
            if validateOnly {
                break
            }
            size = 2
            update(src[n] + src[n+1]<<shift)
        }
    }

Type Swtich

switch也可以用来判断一个变量的类型,switch表达式中声明一个变量,它在每个case子句中具有相应的类型。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

7. 函数[7]

多返回值

go函数和方法支持多返回值。

命名结果参数

返回值可以命名,并像输入参数一样使用。它们的初始值为0。

Defer

defer调用一个函数,使其在被调用函数返回前运行。defer的典型用法是释放资源。defer函数的参数是在defer调用时赋值并被保持住。defer调用的执行顺序是后进先出。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

上面代码的运行结果是4 3 2 1 0

8. 数据[8]

new分配

new(T)返回类型T的新分配的0值对象指针*T

构造器和对象构造方法(复合文字)

构造器就是对象工厂方法分配对象并进行初始化工作。

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    return File{fd, name, nil, 0}
}

go可以返回临时对象指针(go采用垃圾回收机制)。默认对象构造方法必须依次列举所有的成员。采用field:name对方式,可以只列举需要初始化的成员,return &File{fd: fd, name: name}new(File)&File{}相同

make分配

make只能用来创建slicemapchannel,并返回初始化号的对象(不是对象指针)。例如
make([]int, 10, 100)生成一个slice对象,长度是10,容量是100,并指向一个长度为100的int数组。相反,new([]int)返回的0值slice指针并不能使用。

数组

go数组定义需要制定大小,并且大小是类型的一部分。[10]int[20]int是2个不同的类型。go数组是值类型。赋值或传入函数参数时会发生值拷贝。如果需要指针类型数组,一般使用切片。

切片

切片是基于数组的方便使用对象,它具有底层数组引用和当前数组长度以及数组最大长度。切片本身是值对象,但是赋值后,两个切片会指向同一段底层数组,故此能够传递修改。数组支持范围访问n, err := f.Read(buf[0:32])。切片增加元素建议使用内置方法append,它支持自动扩容。

二维数组和切片

type Transform [3][3]float64
type LinesOfText [][]byte
二维切片中的每个切片长度可以不同。分配二维切片有两种方式,如果切片们的长度不同,应该为每个切片单独分配。如果它们的长度一样,可以只分配一次。

  • 单独分配例子,注意每个slice都有独立的make分配
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
    picture[i] = make([]uint8, XSize)
}
  • 分配一次例子,注意底层切片只分配一次,然后把对应段赋给每个切片。
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
    picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

映射

键值对映射是一种很有用的类型。键可以是任何定义了equality操作的类型,注意切片不支持equality。映射也是持有底层数据结构引用,能够传递修改。映射支持复合文字构造,

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}

获取不存在的键值,会返回值类型的0值。如果要明确知道是否存在,使用多返回

var seconds int
var ok bool
seconds, ok = timeZone[tz]

如果只想判断键值是否存在,采用空标记符__, present := timeZone[tz]
删除键值采用内置函数delete,它确保键不存在也能工作。

格式化打印

fmt包提供一系列格式化打印的方法,例如fmt.Printf, fmt.Fprintf, fmt.Sprintf,以及默认格式版本,fmt.Print, fmt.Fprint, fmt.Sprint,默认格式会在每个参数前后插入一个空格。

  • %d 显示数字
  • %v 显示所有类型的值
  • %+v 对于结构体显示字段名
  • %#v 显示对象全部信息
  • %q 显示字符串或字节数组,也可用于数字或rune,显示单引号标记的rune字符
  • %#q 尽可能使用反引号
  • %x 用于字符串、字节数组或数字,显示十六进制值
  • % x 在显示字节前后增加空格
  • %T 显示类型
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

结果
18446744073709551615 ffffffffffffffff; -1 -1

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

结果
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

结果
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}

fmt.Printf("%T\n", timeZone)

结果
map[string] int

自定义显示方法只需重写类型的String() string方法

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

结果
7/-2.35/"abc\tdef"

重写方法时避免无限重入

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

应改为

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

不定参数...

  • 作为参数传入
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}
  • 作为slice使用
func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

Append

append内置函数签名func append(slice []T, elements ...T) []TT表示任何类型。切片通过...方式转换为可变参数。

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

9. 初始化[9]

go的初始化比c和c++更强大,可以构造复杂结构体,不同包之间的初始化顺序也会被正确处理。

常量

go常量在编译时生成,只能是数字、字符、字符串或布尔类型。定义支持常量表达式,例如1<<3math.Sin(math.Pi/4)不支持,因为math.Sin是运行时函数。
iota枚举器用来定义枚举常量

type ByteSize float64
const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)
func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

ByteSize(1e13)显示结果为9.09TB

变量

变量在运行时完成初始化

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

init函数

每个原文件可以定义init函数(支持多个init函数)。init函数会在所有变量初始化完成、所有导入包初始化完成后运行。init函数除了进行初始化外,也常做一些状态验证和修复。

10. 方法[10]

指针 vs 值

任何命名类型都可以定义方法(除了指针和接口)。方法定义需要接收者,它可以是指针也可以是值。值将传递拷贝,方法内做的修改无法影响传入值。指针可以将修改带出方法外。
值接收者的方法可以在值和指针上执行,指针接收者的方法只能在指针上执行。一个特例是,如果值能够转为指针,值上调用指针接收者方法会自动转换为指针再调用。例如,b是值变量且可以取地址,b.Write会被自动重写为(&b).Write

11. 接口和其它类型[11]

接口

和其它语言一样,go的接口是定义对象行为。只要具有接口方法,就可以当接口使用。go的接口一般只有1、2个方法,名字也来源于方法。
一个类型可以实现多个接口,只要它含有指定接口的方法。

转型

T(value)将一个类型值转换成另一个指定类型T,如果两个类型完全一样,此过程并不会产生新值。(int转为float,会产生新值)。

接口转型和类型断言

Type Switch已涉及接口转型,每个case都会转换成对应类型。如果已知接口类型,就要使用类型断言。value.(typeName)。typeName是具体的类型名字,例如str := value.(string)。但如果转型失败,会发生运行时错误。可用下面方式避免错误。

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

如果转型失败,str仍然存在,只是0值。

概述

如果一个类型只是实现了一个接口,并不需要暴露这个类型而应暴露它实现的接口,以隐藏具体的实现细节。这要求构造器返回接口而不是具体实现类型。例如crc32.NewIEEEadler32.New都返回接口hash.Hash32。替换crc32算法为adler32算法,只需要修改构造器调用,而其它代码都保持不变。

接口和方法

只要实现了接口方法的类型就实现了接口,由于几乎所有的类型都可定义方法,故此几乎所有的类型都可以实现接口。
例如Handler接口

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

下面的结构实现了此接口

type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

也可以用整数类型实现这个接口

type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

也可以是其它类型,比如管道

type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}

函数类型可以实现接口,例如

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

http.Handle("/args", http.HandlerFunc(ArgServer))

上面的代码实现了访问/args时,返回系统参数。

12. 空白标识符[12]

空白标识符类似unix中的 /dev/null文件,是一个占位符但不关心它的值。

在多赋值中使用

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path)
}

未使用导入和变量

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader    // For debugging; delete when done.

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
    _ = fd
}

按照约定,这些空白标记符语句必须紧随导入块之后,并且需要提供相应的注释信息,以便将来很容易找到并清除它们。

副作用导入

import _ "net/http/pprof"

导入这个包只是为了运行它的init函数

接口检查

对于运行时接口检查,如果不关心转换值,采用空白标识符忽略转换结果

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

有种罕见情况,需要在编译时确认一个代码中未使用到的接口检查,采用空白标识符忽略转换值。
var _ json.Marshaler = (*RawMessage)(nil)

13. 内嵌[13]

go不支持标准面向对象的继承。go推荐使用组合,并提供内嵌达到类似继承的效果。接口和结构都可使用内嵌。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

接口只能内嵌接口。

type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

type Job struct {
    Command string
    *log.Logger
}

内嵌是在外部类型中定义了一个类型同名字段,并可从外部类型中直接访问内嵌数据。内嵌不支持重载和多态。go通过接口实现多态。需要多态的方法都定义为小接口(go推荐小接口),用组合代替继承。

14. 并发[14]

不同的并发方式

go没有采用资源竞争方式实现并发,而通过共享资源确保每个线程访问各自的资源。总结为一句话
勿以共享内存实现通讯,而以通讯实现共享内存
go的并发方式源于CSP模型(Communicating Sequential Processes)。

go协程

在函数或方法调用前加上go就会启动一个go协程,不同的协程能够并发的运行在同一个代码地址上。协程非常轻量,比分配栈大不了多少。协程可以多路复用操作系统线程,它屏蔽了线程创建、管理等的复杂细节。

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // Note the parentheses - must call the function.
}

go函数支持闭包。

通道

通道由make分配,类似队列类型。make通道是可指定缓存大小,默认为0。通道接收方一直被拥塞直到有数据收到。对于发送方,如果通道中的数据小于缓存,只拥塞到数据复制进通道,如果通道已满(或缓存为0),会一直拥塞到直到有数据被接收。
缓存通道可用作信号量,缓存数量就是控制的并发数量。见下例

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    process(r)  // May take a long time.
    <-sem       // Done; enable next request to run.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // Don't wait for handle to finish.
    }
}

上面的代码有个问题,可能创建无限个协程,但只有MaxOutstanding个运行。改进一下,把创建协程也放入信号量控制中。

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

注意上面代码采用函数闭包将公共变量req的值固定在每个函数调用中。
也可用声明新的局部变量方式完成

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Create new instance of req for the goroutine.
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

函数式编程中,更推荐闭包方式。
还有另一个方法,开启指定数量个处理协程,同时处理。这种方案更自然。

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Start handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Wait to be told to exit.
}

通道类型的通道

通道可以传递任何类型,也包括通道本身。下面例子实现一个简单的RPC。

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

客户端代码

func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)

服务端代码

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

并行

另个应用是将一个复杂计算分散到多个CPU同时运行。见下例

type Vector []float64

// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // signal that this piece is done
}

const numCPU = runtime.GOMAXPROCS(0) // number of CPU cores runtime.NumCPU()

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

务必理解并发和并行的区别。并发是指程序能够独立执行各个模块。并行是指在多个CPU上同时执行运算以提高效率。go是一个并发语言,并不是并行语言,有些并行问题go并不适合。

简单垃圾回收例子

go的并发设计也简化一些非并发问题的解决。如下面这个源于RPC框架的简单垃圾回收例子。

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not.
        select {
        case b = <-freeList:
            // Got one; nothing more to do.
        default:
            // None free, so allocate a new one.
            b = new(Buffer)
        }
        load(b)              // Read next message from the net.
        serverChan <- b      // Send to server.
    }
}

func server() {
    for {
        b := <-serverChan    // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
        case freeList <- b:
            // Buffer on free list; nothing more to do.
        default:
            // Free list full, just carry on.
        }
    }
}

select语句如果找不到满足子句,会执行default子句,这意味着它是非拥塞的。

15. 错误处理[15]

包开发者应该提供丰富的错误信息包含全部的错误信息,比如包名、操作名等。也可以使用类型断言转换为某种指定错误,进一步处理。

运行时错误

常见的错误处理方式是返回它。由调用者判断如何处理。但如果发现一个严重错误无法处理或绕过,调用内置函数panic产生一个运行时错误,panic接收一个任何类型的参数,一般是表示错误信息的字符串。

错误恢复

当运行时错误发生时,无论是显示产生的,还是隐式产生的例如数组下标越界,都会立刻停止当前执行并开始层层退出调用栈,退出前会执行对应的defer函数。可以使用recover方法重新截获运行时错误。recover只能在defer函数中使用,因为退栈时只有defer函数能够执行。例子如下

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

recover只有在defer函数中才可能返回非nil,defer函数中的调用不受panic和recover的影响。错误恢复也用来处理内部错误,下面的例子是regexp包处理解析错误。

// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}
doParse代码

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

一个需要遵守的原则是,内部运行时错误被转成error返回,不要暴露到包外。
使用re-panic重新抛出运行时错误,错误栈中会包含新旧错误信息。


  1. .

  2. .

  3. .

  4. .

  5. .

  6. .

  7. .

  8. .

  9. .

  10. .

  11. .

  12. .

  13. .

  14. .

  15. .

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 1.安装 https://studygolang.com/dl 2.使用vscode编辑器安装go插件 3.go语...
    go含羞草阅读 1,496评论 0 6
  • 环境搭建 Golang在Mac OS上的环境配置 使用Visual Studio Code辅助Go源码编写 VS ...
    陨石坠灭阅读 5,746评论 0 5
  • 原文https://milapneupane.com.np/2019/07/06/learning-golang-...
    Gundy_阅读 393评论 0 2
  • 写在前面 本文是Go语言的快速入门教程,适合于具有一定C语言或者Java语言基础的开发人员,如果您是一位Go...
    foundwei阅读 1,803评论 5 17
  • 从上周五父亲住院已经进入第六天。 今天下午我跟单位同事说一声,去医院陪护。辉宝让我在家睡到三四点再去,两点打来电话...
    平常心_9886阅读 116评论 0 1