我的第一个Go语言程序

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

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 大家好!我的名字叫 Sergey Kamardin。我是来自 Mail.Ru 的一名工程师。这篇文章将讲述我们是如...
    51reboot阅读 1,237评论 0 4
  • Goroutine是Go里的一种轻量级线程——协程。相对线程,协程的优势就在于它非常轻量级,进行上下文切换的代价非...
    witchiman阅读 4,813评论 0 9
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 我与爸爸的关系就像圣女果和西红柿,因为血缘关系所以是一个品种,但其他方面也是大同小异,但却总有说不清的不合拍的...
    深蓝觅海阅读 197评论 0 0