美文网首页
我的第一个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 环境搭建

    一 GO 语言包下载安装 第一个GO程序

  • 【Go语言基础1】运行命令

    1 第一个go程序 写go程序的注意事项: go语言以包作为管理单位 go程序最开始应该声明包 每个程序都必须有一...

  • go 语言第一节课

    第一个 go 语言程序 go 语言与 c 语言的异同点 1.C语言是由于什么组成的?C语言是由函数组成的同样Go语...

  • 学习 Go 语言 1 — 基础语法

    一、第一个 Go 程序 最近在闲来无事之际开始学习点 Go 语言,Go 语言在近几年很火,有 Docker 和 K...

  • go语言基础

    第一个Go程序 go语言的基础知识,包括命名,变量声明,初始化,赋值等等。欢迎来我的个人博客:fizzyi//he...

  • go语言-hello word

    星期天从外边回来,学习久久想学的go语言。程序的第一个仍然是hello word!! go install 安装 ...

  • Go 语言圣经-习题汇总(Go 程序设计语言/The Go Pr

    本文针对 Go 语言圣经 - 《Go 程序设计语言/The Go Programming Language》的所有...

  • 并发编程

    学习来源:Go语言实战、blibli 黑马程序员 20小时快速入门go语言(中) Go语言的优势 Go语言设计简单...

  • 03-第一个Go语言程序-指趣学院

    Go语言程序组成 和C语言程序一样,Go语言程序也是由众多函数组成的 和C语言程序一样,程序运行时系统会自动调用名...

  • 一个Go语言程序示例

    本文档介绍来自《Go语言编程》的简单Go语言程序示例。 程序结构 本程序是一个排序算法的实现,程序结构如下所示 创...

网友评论

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

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