我的第一个Go语言程序

2016-11-27  本文已影响112人  FunFeast

本周帮同事做一个测试用的工具,一个模拟的服务器,根据请求中的用户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语言应该算是一个很大的进步,在大大提升开发效率的同时,也尽可能的保留了高性能。对于互联网行业的后台开发人员,还是很值得一试的。

上一篇下一篇

猜你喜欢

热点阅读