美文网首页
我的第一个Go语言程序

我的第一个Go语言程序

作者: FunFeast | 来源:发表于2016-11-27 21:05 被阅读112次

    本周帮同事做一个测试用的工具,一个模拟的服务器,根据请求中的用户ID从数据库中查找预先设置好的响应返回给客户端。前一段时间看了《Effective Go》《Network Programming with Go》,正想练练手,就决定用Go来写。

    本文涉及的内容:

    1. 网络编程
    2. Json解析
    3. 数据库操作

    准备工作

    安装和配置Go语言开发环境,过程参见官方文档

    安装MySQL驱动:

    $ go get github.com/go-sql-driver/mysql
    

    服务器框架

    Go语言天生就是为服务器开发而设计的,因而对网络接口的封装非常友好。在主函数main()中,调用net.Listen()创建一个Listener监听服务器端口。然后在主循环中用Accept()接受客户端连接请求。Go语言内建了对协程的支持,称作goroutine。在调用函数前加上“go”关键字,就可以创建一个goroutine来执行该函数。这里对每一个客户端连接建立一个协程处理请求。协程可以简化并发编程(concurrent programming)。不过需要注意的是,默认情况下使用goroutine并不能利用多核处理器的并行性来提高性能。Go语言默认对每个进程只使用一个线程,因此即使使用了多个goroutine,在CPU上仍然是串行执行的。如果要使用多线程,需要调用runtime.GOMAXPROCS(NCPU)来设置使用的CPU核数。详请可以参考《Effective Go》中的“并发”一节。

    handleConnection()函数负责从客户端接收请求。由于使用TCP协议,客户端请求以字节流的方式传输,因此服务器端需要进行切包。在这个应用场景里,请求为Json字符串,以0表示结束。每次从客户端连接读取到数据之后,都去查找是否有0值,来确定请求字符串是否接收完。

    package main
    
    import (
        "database/sql"
        _ "github.com/go-sql-driver/mysql"
        "encoding/json"
        "flag"
        "io"
        "log"
        "net"
        "os"
    )
    
    var addr = flag.String("addr", "0.0.0.0:10000", "server address")
    
    func handleConnection(conn net.Conn) {
        request_buf := make([]byte, 1024)
        request_offset := 0
        for {
            if request_offset >= 1024 {
                log.Fatal("receive buffer overflow")
                os.Exit(1)
            }
            readlen, err := conn.Read(request_buf[request_offset:])
            if err == io.EOF {
                log.Println("connection closed")
                conn.Close()
                return
            } else if err != nil {
                log.Println("error reading: ", err.Error())
                conn.Close()
                return
            }
            log.Printf("%d bytes read\n", readlen)
            i := request_offset
            request_offset += readlen
            for ; i < request_offset; i++ {
                if request_buf[i] == 0 {
                    var req Request
                    err := json.Unmarshal(request_buf[:i], &req)
                    if err != nil {
                        log.Printf("failed parsing request: %v, %v\n", request_buf[:i], err.Error())
                    } else {
                        req.conn = conn
                        handleRequest(conn, req)
                    }
                    // if there are any bytes left, move them to the front of the buffer
                    i += 1
                    if i < request_offset {
                        copy(request_buf, request_buf[i:request_offset])
                        request_offset = request_offset - i
                        i = 0
                    } else {
                        request_offset = 0
                        break
                    }
                }
            }
        }
    }
    
    func main() {
        flag.Parse()
    
        log.Println("starting server on: ", *addr)
        l, err := net.Listen("tcp", *addr)
        if err != nil {
            log.Fatal("failed listening", err.Error())
            os.Exit(1)
        }
        defer l.Close()
    
        for {
            // Listen for an incoming connection.
            conn, err := l.Accept()
            if err != nil {
                log.Println("Error accepting: ", err.Error())
            }
            go handleConnection(conn)
        }
    }
    

    请求解析

    包encoding/json里含了对Json串进行编解码函数。使用json.Marshal()可以将一个对象串行化成Json字符串,使用json.Unmarshal()可以将Json字符串反串行化。声明一个Request结构,其成员变量对应想要解析的Json字段,行末的`json:"userid"`指定了成员变量和Json字段的对应关系。注意UserID首字母必须大写,否则在调用Marshal()和Unmarshal()时会被忽略。请求串里包含了多个字段,但是我们只需要userid这一个,因此只也需要一个成员变量。

    type Request struct {
        UserID string `json:"userid"`
    }
    

    如果结构体的成员名字和Json字段的名字一致,比如这里的用户ID在Json串中也叫“UserID”,就可以更简单一点:

    type Request struct {
        UserID string
    }
    

    访问数据库

    Go运行时里包含了对SQL数据库的支持,但是要访问数据库还需要自行安装对应的驱动。这里的数据库是MySQL,驱动安装方法见第1节。

    在main()函数中初始化数据库。db_addr是用于连接数据库的地址,其格式可以看这里。db.Prepare()函数创建一个查询语句,后续可以直接通过这个Stmt对象用不同的参数进行查询。这一步不是必须的,也可以直接调用db.Query()或者db.QueryRow()通过SQL语句进行查询。如果查询语句需要多次被使用的话,还是先Prepare()一下比较好。

    db_addr := *db_user + ":" + *db_pass + "@tcp(" + *db_host + ":" + *db_port + ")/" + *db_name
    db, err := sql.Open("mysql", db_addr)
    if err != nil {
        log.Fatal("failed connecting db: ", err.Error())
        os.Exit(1)
    }
    defer db.Close()
    stmt, err = db.Prepare("SELECT s_response FROM tbmockdata WHERE s_userid = ?")
    if err != nil {
        log.Println("db.Prepare() failed", err.Error())
        os.Exit(1)
    }
    defer stmt.Close()
    

    实现handleRequest()函数。db.QueryRow()从数据库中查询一行数据,返回Row对象。Scan()方法的参数为interface类型,将查询出的数据转换成指定的类型并输出。完成查询之后,将响应写通过conn写回给客户端。

    func handleRequest(conn net.Conn, req Request) {
        log.Println("received request: ", req.UserID)
        var response []byte
        err := stmt.QueryRow(req.UserID).Scan(&response)
        switch {
        case err == sql.ErrNoRows:
            log.Println("no response found")
        case err != nil:
            log.Println("failed query: ", err.Error())
        default:
            log.Println("response: ", string(response))
        }
        response = append(response, 0)
        req.conn.Write(response)
    }
    

    总结

    说一下自己对Go语言的一些理解和体会:

    1. Go语言在语法设计上做了很多新的尝试,有些确实解决了以前用C和C++编程的痛点。比如函数可以有两个返回值,一个是函数的输出,一个是错误信息。在C和C++里编程里,通常只能用某些特殊返回值(比如-1, NULL)表示执行错误,或者是返回值错误码,而真正的输出则通过参数传递出来。另外,defer这个特性很有用。实际开发中常常会碰到这样一种场景:一个操作需要经过若干个步骤才能完成,其中每一个都有可能出错,如果在其中某一步出错,就要取消前面步骤所造成的影响(比如分配内存、打开文件等),然后退出。以前读Linux内核代码时,这种情况特别常见,内核代码都是用goto来解决这个问题的。有了defer,问题就简单多了,比如像下面这样把分配和释放写在一起。

       alloc()
       defer free()
      

      不过Go语言也有些特性我表示不是很能理解。比如声明了变量而没有使用,又或者import了某个package而没有使用,在Go语言里就是一个error而不是warning。像本文中的代码里,import了"github.com/go-sql-driver/mysql"这个package,但是没有显式的调用,这就比较尴尬了。为了解决这个问题,Go语言又引入了“_”这种空白标识符。。。

    1. Goroutine。内建的协程支持部分的解决了并发编程的问题。我们在工作中进行业务开发的时候,也大量的使用到了协程。使用协程进行并发编程的时候,跟写串行程序没有太大区别,使得开发效率大大提升。但是协程并不是万能的。比如我就碰到过一个坑:客户端请求并发量过大,导致服务端创建了大量协程来处理,而每个协程又需要创建一个跟下游服务的连接,导致下游连接数爆掉。这种情况下就需要共享下游连接,协程并不能帮你解决。

    2. 虽然是C语言之父设计的,但是从使用者的角度来看,Go语言更接近Java:自带GC,无需手动分配和释放内存;运行时内建了丰富的函数库,还自带包管理机制,简化开发。相比C/C++来说,Go语言应该算是一个很大的进步,在大大提升开发效率的同时,也尽可能的保留了高性能。对于互联网行业的后台开发人员,还是很值得一试的。

    相关文章

      网友评论

          本文标题:我的第一个Go语言程序

          本文链接:https://www.haomeiwen.com/subject/vypnpttx.html