golang后台开发常用标准库

voidFan · · 289 次点击 · · 开始浏览    

## 一 Go的数据库接口

#### 1.1 接口介绍

Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动,这样做的好处是:框架迁移极其方便。  

Go数据库标准包位于以下两个包中:

- database/sql:提供了保证SQL或类SQL数据库的泛用接口

- database/sql/driver:定义了应被数据库驱动实现的接口,这些接口会被sql包使用

#### 1.2 sql.Register

sql.Register位于database/sql,用来注册数据库驱动当第三方开发者开发数据库驱动时,都会实现init函数,在init里面会调用这个`Register(name string, driver driver.Driver)`完成本驱动的注册。

我们来看一下mysql、sqlite3的驱动里面都是怎么调用的:

```Go

//https://github.com/mattn/go-sqlite3驱动

func init() {

    sql.Register("sqlite3", &SQLiteDriver{})

}

//https://github.com/mikespook/mymysql驱动

// Driver automatically registered in database/sql

var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}

func init() {

    Register("SET NAMES utf8")

    sql.Register("mymysql", &d)

}

```

我们看到第三方数据库驱动都是通过调用这个函数来注册自己的数据库驱动名称以及相应的driver实现。在database/sql内部通过一个map来存储用户定义的相应驱动。

```Go

var drivers = make(map[string]driver.Driver)

drivers[name] = driver

```

因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。

>在我们使用database/sql接口和第三方库的时候经常看到如下:

>       import (

>           "database/sql"

>           _ "github.com/mattn/go-sqlite3"

>       )

>新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们在变量赋值的时候经常看到这个符号,它是用来忽略变量赋值的占位符,那么包引入用到这个符号也是相似的作用,这儿使用`_`的意思是引入后面的包名而不直接使用这个包中定义的函数,变量等资源。

>我们在2.3节流程和函数一节中介绍过init函数的初始化过程,包在引入的时候会自动调用包的init函数以完成对包的初始化。因此,我们引入上面的数据库驱动包之后会自动去调用init函数,然后在init函数里面注册这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。

#### 1.3 driver.Driver

Driver是一个数据库驱动的接口,他定义了一个method: Open(name string),这个方法返回一个数据库的Conn接口。

```Go

type Driver interface {

    Open(name string) (Conn, error)

}

```

返回的Conn只能用来进行一次goroutine的操作,也就是说不能把这个Conn应用于Go的多个goroutine里面。如下代码会出现错误

```Go

...

go goroutineA (Conn)  //执行查询操作

go goroutineB (Conn)  //执行插入操作

...

```

上面这样的代码可能会使Go不知道某个操作究竟是由哪个goroutine发起的,从而导致数据混乱,比如可能会把goroutineA里面执行的查询操作的结果返回给goroutineB从而使B错误地把此结果当成自己执行的插入数据。

第三方驱动都会定义这个函数,它会解析name参数来获取相关数据库的连接信息,解析完成后,它将使用此信息来初始化一个Conn并返回它。

#### 1.4 driver.Conn

Conn是一个数据库连接的接口定义,他定义了一系列方法,这个Conn只能应用在一个goroutine里面,不能使用在多个goroutine里面,详情请参考上面的说明。

```Go

type Conn interface {

    Prepare(query string) (Stmt, error)

    Close() error

    Begin() (Tx, error)

}

```

Prepare函数返回与当前连接相关的执行Sql语句的准备状态,可以进行查询、删除等操作。

Close函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql里面建议的conn pool,所以你不用再去实现缓存conn之类的,这样会容易引起问题。

Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。

#### 1.5 driver.Stmt

Stmt是一种准备好的状态,和Conn相关联,而且只能应用于一个goroutine中,不能应用于多个goroutine。

```Go

type Stmt interface {

    Close() error

    NumInput() int

    Exec(args []Value) (Result, error)

    Query(args []Value) (Rows, error)

}

```

Close函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回rows数据。

NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。

Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据

Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集

#### 1.6 driver.Tx

事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以

```Go

type Tx interface {

    Commit() error

    Rollback() error

}

```

这两个函数一个用来递交一个事务,一个用来回滚事务。

#### 1.7 driver.Execer

这是一个Conn可选择实现的接口

```Go

type Execer interface {

    Exec(query string, args []Value) (Result, error)

}

```

如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。

#### 1.8 driver.Result

这个是执行Update/Insert等操作返回的结果接口定义

```Go

type Result interface {

    LastInsertId() (int64, error)

    RowsAffected() (int64, error)

}

```

LastInsertId函数返回由数据库执行插入操作得到的自增ID号。

RowsAffected函数返回query操作影响的数据条目数。

#### 1.9 driver.Rows

Rows是执行查询返回的结果集接口定义

```Go

type Rows interface {

    Columns() []string

    Close() error

    Next(dest []Value) error

}

```

Columns函数返回查询数据库表的字段信息,这个返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段。

Close函数用来关闭Rows迭代器。

Next函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是driver.Value的值除了string,返回的数据里面所有的string都必须要转换成[]byte。如果最后没数据了,Next函数最后返回io.EOF。

#### 1.10 driver.RowsAffected

RowsAffected其实就是一个int64的别名,但是他实现了Result接口,用来底层实现Result的表示方式

```Go

type RowsAffected int64

func (RowsAffected) LastInsertId() (int64, error)

func (v RowsAffected) RowsAffected() (int64, error)

```

#### 1.11 driver.Value

Value其实就是一个空接口,他可以容纳任何的数据

```Go

type Value interface{}

```

drive的Value是驱动必须能够操作的Value,Value要么是nil,要么是下面的任意一种

```Go

int64

float64

bool

[]byte

string   [*]除了Rows.Next返回的不能是string.

time.Time

```

#### 1.12 driver.ValueConverter

ValueConverter接口定义了如何把一个普通的值转化成driver.Value的接口

```Go

type ValueConverter interface {

    ConvertValue(v interface{}) (Value, error)

}

```

在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个ValueConverter有很多好处:

- 转化driver.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段

- 把数据库查询结果转化成driver.Value值

- 在scan函数里面如何把driver.Value值转化成用户定义的值

#### 1.13 driver.Valuer

Valuer接口定义了返回一个driver.Value的方式

```Go

type Valuer interface {

    Value() (Value, error)

}

```

很多类型都实现了这个Value方法,用来自身与driver.Value的转化。

通过上面的讲解,你应该对于驱动的开发有了一个基本的了解,一个驱动只要实现了这些接口就能完成增删查改等基本操作了,剩下的就是与相应的数据库进行数据交互等细节问题了,在此不再赘述。

#### 1.14 database/sql

database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。

```Go

type DB struct {

    driver   driver.Driver

    dsn      string

    mu       sync.Mutex // protects freeConn and closed

    freeConn []driver.Conn

    closed   bool

}

```

我们可以看到Open函数返回的是DB对象,里面有一个freeConn,它就是那个简易的连接池。它的实现相当简单或者说简陋,就是当执行`db.prepare` -> `db.prepareDC`的时候会`defer dc.releaseConn`,然后调用`db.putConn`,也就是把这个连接放入连接池,每次调用`db.conn`的时候会先判断freeConn的长度是否大于0,大于0说明有可以复用的conn,直接拿出来用就是了,如果不大于0,则创建一个conn,然后再返回之。


## 一 http包运行机制

![](../images/go/net-01.png)

服务端的几个概念:

```

Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息

Response:服务器需要反馈给客户端的信息

Conn:用户的每次请求链接

Handler:处理请求和生成返回信息的处理逻辑

```

http包执行流程:

- 1.创建Listen Socket, 监听指定的端口, 等待客户端请求到来。

- 2.Listen Socket接受客户端的请求, 得到Client Socket, 接下来通过Client Socket与客户端通信。

- 3.处理客户端的请求:首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端。


Go是通过一个函数`ListenAndServe`来处理这些事情的,这个底层其实这样处理的:初始化一个server对象,然后调用了`net.Listen("tcp", addr)`,也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口。

源码如下:

```Go

func (srv *Server) Serve(l net.Listener) error {

    defer l.Close()

    var tempDelay time.Duration // how long to sleep on accept failure

    for {

        rw, e := l.Accept()

        if e != nil {

            if ne, ok := e.(net.Error); ok && ne.Temporary() {

                if tempDelay == 0 {

                    tempDelay = 5 * time.Millisecond

                } else {

                    tempDelay *= 2

                }

                if max := 1 * time.Second; tempDelay > max {

                    tempDelay = max

                }

                log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)

                time.Sleep(tempDelay)

                continue

            }

            return e

        }

        tempDelay = 0

        c, err := srv.newConn(rw)

        if err != nil {

            continue

        }

        go c.serve()

    }

}

```

监控之后如何接收客户端的请求呢?上面代码执行监控端口之后,调用了`srv.Serve(net.Listener)`函数,这个函数就是处理接收客户端的请求信息。这个函数里面起了一个`for{}`,首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个goroutine,把这个请求的数据当做参数扔给这个conn去服务:`go c.serve()`。这个就是高并发体现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响。  

那么如何具体分配到相应的函数来处理请求呢?conn首先会解析request:`c.readRequest()`,然后获取相应的handler:`handler := c.server.Handler`,也就是我们刚才在调用函数`ListenAndServe`时候的第二个参数,我们前面例子传递的是nil,也就是为空,那么默认获取`handler = DefaultServeMux`,那么这个变量用来做什么的呢?对,这个变量就是一个路由器,它用来匹配url跳转到其相应的handle函数,那么这个我们有设置过吗?有,我们调用的代码里面第一句不是调用了`http.HandleFunc("/", sayhelloName)`嘛。这个作用就是注册了请求`/`的路由规则,当请求uri为"/",路由就会转到函数sayhelloName,DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName本身,最后通过写入response的信息反馈到客户端。

![](../images/go/net-02.png)

## 二 http包详解

Go的http有两个核心功能:Conn、ServeMux。  

与我们一般编写的http服务器不同, Go为了实现高并发和高性能, 使用了goroutines来处理Conn的读写事件, 这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。这是Go高效的保证。

Go在等待客户端请求里面是这样写的:

```Go

c, err := srv.newConn(rw)

if err != nil {

    continue

}

go c.serve()

```

这里我们可以看到客户端的每次请求都会创建一个Conn,这个Conn里面保存了该次请求的信息,然后再传递到对应的handler,该handler中便可以读取到相应的header信息,这样保证了每个请求的独立性。

conn.server内部是调用了http包默认的路由器,通过路由器把本次请求的信息传递到了后端的处理函数。那么这个路由器是怎么实现的呢?

它的结构如下:

```Go

type ServeMux struct {

    mu sync.RWMutex   //锁,由于请求涉及到并发处理,因此这里需要一个锁机制

    m  map[string]muxEntry  // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式

    hosts bool // 是否在任意的规则中带有host信息

}

```

下面看一下muxEntry

```Go

type muxEntry struct {

    explicit bool   // 是否精确匹配

    h        Handler // 这个路由表达式对应哪个handler

    pattern  string  //匹配字符串

}

```

接着看一下Handler的定义

```Go

type Handler interface {

    ServeHTTP(ResponseWriter, *Request)  // 路由实现器

}

```

Handler是一个接口,在http包里面还定义了一个类型`HandlerFunc`,默认就实现了ServeHTTP这个接口,即我们调用了HandlerFunc(f)

```Go

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).

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

    f(w, r)

}

```

路由器里面存储好了相应的路由规则之后,那么具体的请求又是怎么分发的呢?请看下面的代码,默认的路由器实现了`ServeHTTP`:

```Go

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {

    if r.RequestURI == "*" {

        w.Header().Set("Connection", "close")

        w.WriteHeader(StatusBadRequest)

        return

    }

    h, _ := mux.Handler(r)

    h.ServeHTTP(w, r)

}

```

如上所示路由器接收到请求之后,如果是`*`那么关闭链接,不然调用`mux.Handler(r)`返回对应设置路由的处理Handler,然后执行`h.ServeHTTP(w, r)`

也就是调用对应路由的handler的ServerHTTP接口,那么mux.Handler(r)怎么处理的呢?

```Go

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

    if r.Method != "CONNECT" {

        if p := cleanPath(r.URL.Path); p != r.URL.Path {

            _, pattern = mux.handler(r.Host, p)

            return RedirectHandler(p, StatusMovedPermanently), pattern

        }

    }   

    return mux.handler(r.Host, r.URL.Path)

}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {

    mux.mu.RLock()

    defer mux.mu.RUnlock()

    // Host-specific pattern takes precedence over generic ones

    if mux.hosts {

        h, pattern = mux.match(host + path)

    }

    if h == nil {

        h, pattern = mux.match(path)

    }

    if h == nil {

        h, pattern = NotFoundHandler(), ""

    }

    return

}

```

原来他是根据用户请求的URL和路由器里面存储的map去匹配的,当匹配到之后返回存储的handler,调用这个handler的ServeHTTP接口就可以执行到相应的函数了。

通过上面这个介绍,我们了解了整个路由过程,Go其实支持外部实现的路由器 `ListenAndServe`的第二个参数就是用以配置外部路由器的,它是一个Handler接口,即外部路由器只要实现了Handler接口就可以,我们可以在自己实现的路由器的ServeHTTP里面实现自定义路由功能。

如下代码所示,我们自己实现了一个简易的路由器

```Go

package main

import (

    "fmt"

    "net/http"

)

type MyMux struct {

}

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {

    if r.URL.Path == "/" {

        sayhelloName(w, r)

        return

    }

    http.NotFound(w, r)

    return

}

func sayhelloName(w http.ResponseWriter, r *http.Request) {

    fmt.Fprintf(w, "Hello myroute!")

}

func main() {

    mux := &MyMux{}

    http.ListenAndServe(":9090", mux)

}

```

Go代码执行流程梳理:

- 首先调用Http.HandleFunc

    按顺序做了几件事:

    1 调用了DefaultServeMux的HandleFunc

    2 调用了DefaultServeMux的Handle

    3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则

- 其次调用http.ListenAndServe(":9090", nil)

    按顺序做了几件事情:

    1 实例化Server

    2 调用Server的ListenAndServe()

    3 调用net.Listen("tcp", addr)监听端口

    4 启动一个for循环,在循环体中Accept请求

    5 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve()

    6 读取每个请求的内容w, err := c.readRequest()

    7 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux

    8 调用handler的ServeHttp

    9 在这个例子中,下面就进入到DefaultServeMux.ServeHttp

    10 根据request选择handler,并且进入到这个handler的ServeHTTP

        mux.handler(r).ServeHTTP(w, r)

    11 选择handler:

    A 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry)

    B 如果有路由满足,调用这个路由handler的ServeHTTP

    C 如果没有路由满足,调用NotFoundHandler的ServeHTTP


## 一 文件操作

#### 1.1 目录操作

文件操作的大多数函数都是在os包里面,下面列举了几个目录操作的:

- func Mkdir(name string, perm FileMode) error

    创建名称为name的目录,权限设置是perm,例如0777


- func MkdirAll(path string, perm FileMode) error

    根据path创建多级子目录


- func Remove(name string) error

    删除名称为name的目录,当目录下有文件或者其他目录时会出错

- func RemoveAll(path string) error

    根据path删除多级子目录,如果path是单个名称,那么该目录下的子目录全部删除。

实例:

```Go

package main

import (

    "fmt"

    "os"

)

func main() {

    os.Mkdir("test", 0777)

    os.MkdirAll("test/test1/test2", 0777)

    err := os.Remove("test")

    if err != nil {

        fmt.Println(err)

    }

    os.RemoveAll("test")

}

```

#### 1.2 新建文件

新建文件可以通过如下两个方法

- func Create(name string) (file *File, err Error)

    根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666的文件,返回的文件对象是可读写的。

- func NewFile(fd uintptr, name string) *File


    根据文件描述符创建相应的文件,返回一个文件对象

#### 1.3 打开文件

- func Open(name string) (file *File, err Error)

    该方法打开一个名称为name的文件,但是是只读方式,内部实现其实调用了OpenFile。

- func OpenFile(name string, flag int, perm uint32) (file *File, err Error) 

    打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限      

#### 1.4 写文件

写文件函数:

- func (file *File) Write(b []byte) (n int, err Error)

    写入byte类型的信息到文件

- func (file *File) WriteAt(b []byte, off int64) (n int, err Error)

    在指定位置开始写入byte类型的信息

- func (file *File) WriteString(s string) (ret int, err Error)

    写入string信息到文件


写文件的示例代码

```Go

package main

import (

    "fmt"

    "os"

)

func main() {

    userFile := "test.txt"

    fout, err := os.Create(userFile)        

    if err != nil {

        fmt.Println(userFile, err)

        return

    }

    defer fout.Close()

    for i := 0; i < 10; i++ {

        fout.WriteString("Just a test!\r\n")

        fout.Write([]byte("Just a test!\r\n"))

    }

}

```

带缓冲的写入:

```go

file, err := os.Openfile(path, O_WRONLY | O_CREATE, 0666)

if err != nil {

    fmt.Printf("%v", err)

    return

}

defer file.Close()

writer := bufio.NewWriter(file)

for l := 0; i < 5; i++ {

    writer.Writetring("hello\n")

}

writer.Flush()

```

#### 1.5 读文件

读文件函数:

- func (file *File) Read(b []byte) (n int, err Error)

    读取数据到b中

- func (file *File) ReadAt(b []byte, off int64) (n int, err Error)

    从off开始读取数据到b中

读文件的示例代码:

```Go

package main

import (

    "fmt"

    "os"

)

func main() {

    userFile := "test.txt"

    fl, err := os.Open(userFile)        

    if err != nil {

        fmt.Println(userFile, err)

        return

    }

    defer fl.Close()                    //当程序退出时,defer,需要关闭文件,否则容易产生内存泄漏

    buf := make([]byte, 1024)

    for {

        n, _ := fl.Read(buf)

        if 0 == n {

            break

        }

        os.Stdout.Write(buf[:n])

    }

}

```

带缓冲的大文件读取:

```go

    userFile := "test.txt"

    fl, err := os.Open(userFile)        

    if err != nil {

        fmt.Println(userFile, err)

        return

    }

    defer fl.Close()

    reader := bufio.NewReader(file)

    for {

        str, err := reader.ReadString("\n")     //读到换行就结束一次

        if err != io.EOF {                      //io.EOF表示问价末尾

            break

        }

        //输出内容

        fmt.Print(str)

    }

```

一次性读取小型文件到内存中,该方法内部封装了open和close:

```

file := "d:/test.txt"

content, err := ioutil.ReadFile(file)

if err != nil {

    fmt.Printf("%v",err)

}

fmt.Prinf("%v",string(content))

```


## 通过正则判断是否匹配

`regexp`包中含有三个函数用来判断是否匹配,如果匹配返回true,否则返回false

```Go

func Match(pattern string, b []byte) (matched bool, error error)

func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)

func MatchString(pattern string, s string) (matched bool, error error)

```

上面的三个函数实现了同一个功能,就是判断`pattern`是否和输入源匹配,匹配的话就返回true,如果解析正则出错则返回error。三个函数的输入源分别是byte slice、RuneReader和string。

如果要验证一个输入是不是IP地址,那么如何来判断呢?请看如下实现

```Go

func IsIP(ip string) (b bool) {

    if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {

        return false

    }

    return true

}

```

可以看到,`regexp`的pattern和我们平常使用的正则一模一样。再来看一个例子:当用户输入一个字符串,我们想知道是不是一次合法的输入:

```Go

func main() {

    if len(os.Args) == 1 {

        fmt.Println("Usage: regexp [string]")

        os.Exit(1)

    } else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m {

        fmt.Println("数字")

    } else {

        fmt.Println("不是数字")

    }

}

```

在上面的两个小例子中,我们采用了Match(Reader|String)来判断一些字符串是否符合我们的描述需求,它们使用起来非常方便。

## 通过正则获取内容

Match模式只能用来对字符串的判断,而无法截取字符串的一部分、过滤字符串、或者提取出符合条件的一批字符串。如果想要满足这些需求,那就需要使用正则表达式的复杂模式。

我们经常需要一些爬虫程序,下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据:

```Go

package main

import (

    "fmt"

    "io/ioutil"

    "net/http"

    "regexp"

    "strings"

)

func main() {

    resp, err := http.Get("http://www.baidu.com")

    if err != nil {

        fmt.Println("http get error.")

    }

    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)

    if err != nil {

        fmt.Println("http read error")

        return

    }

    src := string(body)

    //将HTML标签全转换成小写

    re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")

    src = re.ReplaceAllStringFunc(src, strings.ToLower)

    //去除STYLE

    re, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")

    src = re.ReplaceAllString(src, "")

    //去除SCRIPT

    re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")

    src = re.ReplaceAllString(src, "")

    //去除所有尖括号内的HTML代码,并换成换行符

    re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")

    src = re.ReplaceAllString(src, "\n")

    //去除连续的换行符

    re, _ = regexp.Compile("\\s{2,}")

    src = re.ReplaceAllString(src, "\n")

    fmt.Println(strings.TrimSpace(src))

}

```

从这个示例可以看出,使用复杂的正则首先是Compile,它会解析正则表达式是否合法,如果正确,那么就会返回一个Regexp,然后就可以利用返回的Regexp在任意的字符串上面执行需要的操作。

解析正则表达式的有如下几个方法:

```Go

func Compile(expr string) (*Regexp, error)

func CompilePOSIX(expr string) (*Regexp, error)

func MustCompile(str string) *Regexp

func MustCompilePOSIX(str string) *Regexp

```

CompilePOSIX和Compile的不同点在于POSIX必须使用POSIX语法,它使用最左最长方式搜索,而Compile是采用的则只采用最左方式搜索(例如[a-z]{2,4}这样一个正则表达式,应用于"aa09aaa88aaaa"这个文本串时,CompilePOSIX返回了aaaa,而Compile的返回的是aa)。前缀有Must的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接panic,而不加Must的则只是返回错误。

在了解了如何新建一个Regexp之后,我们再来看一下这个struct提供了哪些方法来辅助我们操作字符串,首先我们来看下面这些用来搜索的函数:

```Go

func (re *Regexp) Find(b []byte) []byte

func (re *Regexp) FindAll(b []byte, n int) [][]byte

func (re *Regexp) FindAllIndex(b []byte, n int) [][]int

func (re *Regexp) FindAllString(s string, n int) []string

func (re *Regexp) FindAllStringIndex(s string, n int) [][]int

func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string

func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int

func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte

func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int

func (re *Regexp) FindIndex(b []byte) (loc []int)

func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int)

func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int

func (re *Regexp) FindString(s string) string

func (re *Regexp) FindStringIndex(s string) (loc []int)

func (re *Regexp) FindStringSubmatch(s string) []string

func (re *Regexp) FindStringSubmatchIndex(s string) []int

func (re *Regexp) FindSubmatch(b []byte) [][]byte

func (re *Regexp) FindSubmatchIndex(b []byte) []int

```

上面这18个函数我们根据输入源(byte slice、string和io.RuneReader)不同还可以继续简化成如下几个,其他的只是输入源不一样,其他功能基本是一样的:

```Go

func (re *Regexp) Find(b []byte) []byte

func (re *Regexp) FindAll(b []byte, n int) [][]byte

func (re *Regexp) FindAllIndex(b []byte, n int) [][]int

func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte

func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int

func (re *Regexp) FindIndex(b []byte) (loc []int)

func (re *Regexp) FindSubmatch(b []byte) [][]byte

func (re *Regexp) FindSubmatchIndex(b []byte) []int

```

对于这些函数的使用我们来看下面这个例子

```Go

package main

import (

    "fmt"

    "regexp"

)

func main() {

    a := "I am learning Go language"

    re, _ := regexp.Compile("[a-z]{2,4}")

    //查找符合正则的第一个

    one := re.Find([]byte(a))

    fmt.Println("Find:", string(one))

    //查找符合正则的所有slice,n小于0表示返回全部符合的字符串,不然就是返回指定的长度

    all := re.FindAll([]byte(a), -1)

    fmt.Println("FindAll", all)

    //查找符合条件的index位置,开始位置和结束位置

    index := re.FindIndex([]byte(a))

    fmt.Println("FindIndex", index)

    //查找符合条件的所有的index位置,n同上

    allindex := re.FindAllIndex([]byte(a), -1)

    fmt.Println("FindAllIndex", allindex)

    re2, _ := regexp.Compile("am(.*)lang(.*)")

    //查找Submatch,返回数组,第一个元素是匹配的全部元素,第二个元素是第一个()里面的,第三个是第二个()里面的

    //下面的输出第一个元素是"am learning Go language"

    //第二个元素是" learning Go ",注意包含空格的输出

    //第三个元素是"uage"

    submatch := re2.FindSubmatch([]byte(a))

    fmt.Println("FindSubmatch", submatch)

    for _, v := range submatch {

        fmt.Println(string(v))

    }

    //定义和上面的FindIndex一样

    submatchindex := re2.FindSubmatchIndex([]byte(a))

    fmt.Println(submatchindex)

    //FindAllSubmatch,查找所有符合条件的子匹配

    submatchall := re2.FindAllSubmatch([]byte(a), -1)

    fmt.Println(submatchall)

    //FindAllSubmatchIndex,查找所有字匹配的index

    submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1)

    fmt.Println(submatchallindex)

}

```

前面介绍过匹配函数,Regexp也定义了三个函数,它们和同名的外部函数功能一模一样,其实外部函数就是调用了这Regexp的三个函数来实现的:

```Go

func (re *Regexp) Match(b []byte) bool

func (re *Regexp) MatchReader(r io.RuneReader) bool

func (re *Regexp) MatchString(s string) bool

```

接下里让我们来了解替换函数是怎么操作的?

```Go

func (re *Regexp) ReplaceAll(src, repl []byte) []byte

func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte

func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte

func (re *Regexp) ReplaceAllLiteralString(src, repl string) string

func (re *Regexp) ReplaceAllString(src, repl string) string

func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string

```

这些替换函数我们在上面的抓网页的例子有详细应用示例,

接下来我们看一下Expand的解释:

```Go

func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte

func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte

```

那么这个Expand到底用来干嘛的呢?请看下面的例子:

```Go

func main() {

    src := []byte(`

        call hello alice

        hello bob

        call hello eve

    `)

    pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`)

    res := []byte{}

    for _, s := range pat.FindAllSubmatchIndex(src, -1) {

        res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)

    }

    fmt.Println(string(res))

}

```


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:voidFan

查看原文:golang后台开发常用标准库

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:1006366459

289 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传