单元测试

测试文件需要以xxx_test.go的方式命名。

go test默认会执行当前目录下的所有测试文件。对于执行某个特定测试文件下的特定测试函数,参见-file和-run参数。

测试文件模板:

package gotest

import (
    "testing"
)

func Test_Division_1(t *testing.T) {
    if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
        t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错
    } else {
        t.Log("第一个测试通过了") //记录一些你期望记录的信息
    }
}

func Test_Division_2(t *testing.T) {
    t.Error("就是不通过")
}

测试函数是func TestXxx(t *testing.T) {}格式。

记录信息:t.Log("one test passed.", e)

产生错误信息:t.Error("Division did not work as expected.")

继续阅读

Go语言中没有直接的大小端检测库,因此我写了个小程序用来检测机器的大小端。

检测的核心方法其实就是C语言里的检测方法,通过测试一个特定的int类型变量的头部字节,来确定机器的大小端。

核心代码:

const INT_SIZE int = int(unsafe.Sizeof(0))

//true = big endian, false = little endian
func getEndian() (ret bool) {
    var i int = 0x1
    bs := (*[INT_SIZE]byte)(unsafe.Pointer(&i))
    if bs[0] == 0 {
        return true
    } else {
        return false
    }
}

我写的检测代码的地址:

https://github.com/virtao/GoEndian

通过以下命令安装到gopath里:

go get github.com/virtao/GoEndian

库的使用方法在项目主页中有描述。

Go中可以使用“+”合并字符串,但是这种合并方式效率非常低,每合并一次,就必须遍历一次字符串。Java中提供StringBuilder类来解决这个问题。Go中也有类似的机制,那就是Buffer。以下是示例代码:

package main

import (
    \"bytes\"
    \"fmt\"
)

func main() {
    var buffer bytes.Buffer

    for i := 0; i < 1000; i++ {
        buffer.WriteString(\"a\")
    }

    fmt.Println(buffer.String())
}

使用bytes.Buffer来组装字符串,不需要遍历,只需要将添加的字符串放在缓存末尾即可。

那么这种效率差别有多大呢?我们做个测试,组装10万个“a”。

package main

import (
    \"bytes\"
    \"fmt\"
    \"time\"
)

func main() {

    _, t := testBuf()

    //fmt.Println(s)
    fmt.Println(\"string buffer : \", t, \"ns\")

    _, t = testPlus()

    //fmt.Println(ss)
    fmt.Println(\"string plus : \", t, \"ns\")
}

func testPlus() (s string, t int64) {
    start := time.Now().UnixNano()
    for i := 0; i < 100000; i++ {
        s += \"a\"
    }
    end := time.Now().UnixNano()

    return s, end - start
}

func testBuf() (s string, t int64) {
    var buf bytes.Buffer
    start := time.Now().UnixNano()
    for i := 0; i < 100000; i++ {
        buf.WriteString(\"a\")
    }
    s = buf.String()
    end := time.Now().UnixNano()

    return s, end - start
}

测试结果:

string buffer :  3001700 ns
string plus :  1414015200 ns

使用Buffer仅仅用了3ms,而加号则使用了1.4s,差距巨大。而且使用Buffer时,三分之二的时间用在了buf.String()上,也就是说,组装时间仅仅有1ms,其余2ms花在了转换为字符串上。

我本来是组装1万个“a”的,结果使用Buffer的方式测不出时间(时间为0ns),不得已采用的10万,这也说明Buffer效率非常高。

Go的slice的实质是对数组的封装。slice在数组的基础上,扩展出了大小可变的功能,但是底层实现依赖于数组。而且其切片功能非常类似于指针:切片是一个slice对数组内容的指向,而不是对数组内容的复制。以下程序说明了这种不同。

例子

package main

import (
    \"fmt\"
)

func main() {
    arr1 := [5]int{1, 2, 3, 4, 5}
    fmt.Println(\"定义数组arr1:\", arr1)
    slice1 := arr1[1:2]
    fmt.Println(\"定义对数组arr1的切片slice1(arr1[1:2]):\", slice1)
    fmt.Println(\"slice1缓冲大小:\", cap(slice1))
    slice1 = append(slice1, 6, 7, 8)
    fmt.Println(\"对slice1进行append,添加6、7、8三个数字,这时slice1内容:\", slice1)
    fmt.Println(\"此时slice1缓冲大小:\", cap(slice1))
    fmt.Println(\"这时arr1的内容:\", arr1)
    fmt.Println(\"这说明slice1仅仅是指向arr1的指针,如果修改了slice1,会对之前的arr1造成影响,这也是为什么slice1非常轻量高效的原因。\")
    fmt.Println(\"=========\")

    arr2 := [5]int{1, 2, 3, 4, 5}
    fmt.Println(\"定义数组arr2:\", arr2)
    slice2 := arr2[1:2]
    fmt.Println(\"定义对数组arr2的切片slice2(arr2[1:2]):\", slice2)
    fmt.Println(\"slice2缓冲大小:\", cap(slice2))
    slice2 = append(slice2, 6, 7, 8, 9)
    fmt.Println(\"对slice2进行append,添加6、7、8、9四个数字,这时slice2内容:\", slice2)
    fmt.Println(\"此时slice2缓冲大小:\", cap(slice2))
    fmt.Println(\"这时arr2的内容:\", arr2)
    fmt.Println(\"这说明之前的结论部分正确,也就是说,如果slice2执行append操作时超过了原有缓冲区的大小,slice2会重新创建一个新的更大的缓冲区(新大小为原先缓冲区大小的两倍),并将原数据复制到新缓冲区。这也是为什么arr2内容没有变化的原因。\")
    fmt.Println(\"PS:这里所说slice的缓冲区,其实质是个数组,slice永远指向一个数组。\")
    fmt.Println(\"=========\")

    slice3 := []int{1, 2, 3, 4, 5}
    fmt.Println(\"创建切片slice3:\", slice3)
    fmt.Println(\"slice3缓冲大小:\", cap(slice3))
    slice3 = append(slice3, 6, 7, 8)
    fmt.Println(\"为slice3添加6、7、8三个数:\", slice3)
    fmt.Println(\"slice3缓冲大小:\", cap(slice3))
    fmt.Println(\"这说明slice是以原大小的倍数增长的。\")
    fmt.Println(\"=========\")

    fmt.Println(\"创建空的数组arr4和切片slice4。\")
    arr4 := [5]int{}
    slice4 := []int{}
    fmt.Println(\"数组arr4初始值:\", arr4)
    fmt.Println(\"切片slice4初始值:\", slice4)
    fmt.Println(\"切片slice4的缓冲区大小:\", cap(slice4))
    fmt.Println(\"以上展示了array与slice的区别。\")
}

运行结果:

定义数组arr1: [1 2 3 4 5]
定义对数组arr1的切片slice1(arr1[1:2]): [2]
slice1缓冲大小: 4
对slice1进行append,添加6、7、8三个数字,这时slice1内容: [2 6 7 8]
此时slice1缓冲大小: 4
这时arr1的内容: [1 2 6 7 8]
这说明slice1仅仅是指向arr1的指针,如果修改了slice1,会对之前的arr1造成影响,这也是为什么slice1非常轻量高效的原因。
=========
定义数组arr2: [1 2 3 4 5]
定义对数组arr2的切片slice2(arr2[1:2]): [2]
slice2缓冲大小: 4
对slice2进行append,添加6、7、8、9四个数字,这时slice2内容: [2 6 7 8 9]
此时slice2缓冲大小: 8
这时arr2的内容: [1 2 3 4 5]
这说明之前的结论部分正确,也就是说,如果slice2执行append操作时超过了原有缓冲区的大小,slice2会重新创建一个新的更大的缓冲区(新大小为原先缓冲区大小的两倍),并将原数据复制到新缓冲区。这也是为什么arr2内容没有变化的原因。
PS:这里所说slice的缓冲区,其实质是个数组,slice永远指向一个数组。
=========
创建切片slice3: [1 2 3 4 5]
slice3缓冲大小: 5
为slice3添加6、7、8三个数: [1 2 3 4 5 6 7 8]
slice3缓冲大小: 10
这说明slice是以原大小的倍数增长的。
=========
创建空的数组arr4和切片slice4。
数组arr4初始值: [0 0 0 0 0]
切片slice4初始值: []
切片slice4的缓冲区大小: 0
以上展示了array与slice的区别。

总结

  • slice类似于一个指向数组的指针,slice与数组的不同之处在于,slice可以改变大小。而slice改变大小的方式是重新创建一个更大的数组,并将数据复制到新数组。
  • slice的大小是以原大小的2倍来增长的。
  • 切片后的数据尽量用于读取而不是写入,时刻记住切出来的数据是对原数据的一个引用,而不是一个copy。
  • slice和array的定义方式区别很小,仅仅是是否指定了大小的区别。例如:
    array := [5]int{} //指定了大小,这是一个数组
    slice := []int{} //未指定大小,这是一个切片
    

    如何检验一个变量是否为数组,只需要对其进行append操作,对数组进行append操作会出错。array默认值为5个0元素,而slice默认为空。

  • 我猜测,对于同样多元素,slice要比array占用内存多,因为slice本身需要有个变量来记录其实际长度(slice缓冲大小和slice的有效长度可能不一致)。

Go中所有的文本都以UTF-8编码,因此使用GBK、EUC-KR等国家编码的文本需要进行编码转换才能使用。Go有个比较不错的编码转换库:mahonia(http://code.google.com/p/mahonia/),我们通过例子就可以很方便的了解它的使用方法。

从GBK与UTF-8互转的例子,代码如下:

package main

import (
    \"code.google.com/p/mahonia\"
    \"fmt\"
)

func main() {
    //\"你好,世界!\"的GBK编码
    testBytes := []byte{0xC4, 0xE3, 0xBA, 0xC3, 0xA3, 0xAC, 0xCA, 0xC0, 0xBD, 0xE7, 0xA3, 0xA1}
    var testStr string
    utfStr := \"你好,世界!\"
    var dec mahonia.Decoder
    var enc mahonia.Encoder

    testStr = string(testBytes)

    dec = mahonia.NewDecoder(\"gbk\")
    if ret, ok := dec.ConvertStringOK(testStr); ok {
        fmt.Println(\"GBK to UTF-8: \", ret, \" bytes:\", testBytes)
    }

    enc = mahonia.NewEncoder(\"gbk\")
    if ret, ok := enc.ConvertStringOK(utfStr); ok {
        fmt.Println(\"UTF-8 to GBK: \", ret, \" bytes: \", []byte(ret))
    }
    return
}

这里要注意,NewDecoder和NewEncoder在指定的编码集不存在的情况下,会返回nil,当然,如果你确定编码集是存在的,可以忽略。对于编码集对应的名称,可以查看mahonia的源码,分布在各个编码源文件的init方法里。比如,GBK编码在gbk.go文件里。

Go中的空接口可以代表任何类型,这时候我们就需要有一种方法,能够判断接口代表的具体类型,并且能够进行转换。Go中有两种方法可以实现这个目标。

if结构

if ret, ok := val.(string); ok {
    return ret
}
return def

val是空接口。我们通过这个结构,可以判断val里面是否保存了string类型。如果是,则ok = true,ret保存转换后的字符串;如果不是,则ok = false,ret为空。使用这种结构我们很容易判断并转换类型。

switch结构

有时候我们需要大量的类型判断,这时候用if就不是那么方便了(需要套好几层if语句),其实switch也是支持类型判断的,代码示例如下:

switch v := val.(type) {
case int:
    fmt.Println(\"type is int, value is \", v)
case string:
    fmt.Println(\"type is string, value is \", v)
default:
    fmt.Println(\"unknown type\")
}

case条件全部是类型,v是转换后的值。注意,“val.(type)”这种写法只能用在switch语句中。另外,这种switch结构不能使用fallthrough。

示例代码

package main

import (
    \"fmt\"
)

func main() {
    var test interface{}

    test1 := 20
    test2 := \"Hello World\"

    fmt.Println(\"test1 = \", test1)
    fmt.Println(\"test2 = \", test2)

    test = test1
    fmt.Println(\"MustInt: test1 = \", MustInt(test, 0))
    fmt.Println(\"MustString: test1 = \", MustString(test, \"not string\"))
    PrintType(test)

    fmt.Println()

    test = test2
    fmt.Println(\"MustInt: test2 = \", MustInt(test, 0))
    fmt.Println(\"MustString: test2 = \", MustString(test, \"not string\"))
    PrintType(test)
}

func MustString(val interface{}, def string) string {
    if ret, ok := val.(string); ok {
        return ret
    }
    return def
}

func MustInt(val interface{}, def int) int {
    if ret, ok := val.(int); ok {
        return ret
    }
    return def
}

func PrintType(val interface{}) {
    switch v := val.(type) {
    case int:
        fmt.Println(\"type is int, value is \", v)
    case string:
        fmt.Println(\"type is string, value is \", v)
    default:
        fmt.Println(\"unknown type\")
    }
}

运行结果:

test1 =  20
test2 =  Hello World
MustInt: test1 =  20
MustString: test1 =  not string
type is int, value is  20

MustInt: test2 =  0
MustString: test2 =  Hello World
type is string, value is  Hello World

Go的数据库驱动已经非常多了,由于我要连接Microsoft SQL Server,因此需要ODBC的支持。这里我是用了go-odbc库:

https://github.com/weigj/go-odbc

本文最后会再列出几种库来。

此库使用方法很简单,只要正确配置ODBC源,基本没什么问题。下面是其自带的例子,执行基本的预处理SQL语句:

package main

import (
    \"odbc\"
)

func main() {
    conn, _ := odbc.Connect(\"DSN=dsn;UID=user;PWD=password\")
    stmt, _ := conn.Prepare(\"select * from user where username = ?\")
    stmt.Execute(\"admin\")
    rows, _ := stmt.FetchAll()
    for i, row := range rows {
        println(i, row)
    }
    stmt.Close()
    conn.Close()
}

下面是使用事务的方式,并且未使用预处理SQL:

package main

import (
    odbc \"./go-odbc\"
    \"errors\"
    \"fmt\"
    \"strconv\"
    \"time\"
)

func main() {
    start := time.Now().UnixNano()
    defer func() {
        end := time.Now().UnixNano()
        fmt.Println(\"耗时:\" + strconv.FormatInt((end-start)/1000000, 10) + \"ms\")
    }()

    err := loadFile2DB(\"DSN=LocalSQLServer;UID=DBUser;PWD=123456\", \"dbo.odbctest\", \"C:\\temp\\odbcTest\\testdata.txt\")
    if err != nil {
        fmt.Println(err.Error())
    }
}

func loadFile2DB(dsn string, tableName string, dataFile string) (err error) {
    fmt.Println(\"开始导入数据库,连接字符串:\" + dsn + \"表名称:\" + tableName + \",数据文件路径:\" + dataFile)

    //DSN要在“数据源(ODBC)”里创建,在选择服务器的时候,有可能找不到任何SQL服务器,这时候使用“.”表示本机服务器即可。
    var conn *odbc.Connection
    var stmt *odbc.Statement
    var odbcerr *odbc.ODBCError
    var sql string

    if conn, odbcerr = odbc.Connect(dsn); odbcerr != nil {
        err = errors.New(\"连接数据库失败!SQLState: \" + odbcerr.SQLState + \", ErrorMessage: \" + odbcerr.ErrorMessage)
        return
    }
    defer conn.Close()

    conn.BeginTransaction()

    sql = `BULK
INSERT ` + tableName + ` 
FROM \"` + dataFile + `\" 
WITH
(
FIELDTERMINATOR = \',\',
ROWTERMINATOR = \'n\'
)`
    stmt, odbcerr = conn.ExecDirect(sql)
    if odbcerr != nil {
        err = errors.New(\"查询语句执行失败!SQLState: \" + odbcerr.SQLState + \", ErrorMessage: \" + odbcerr.ErrorMessage)
        conn.Rollback()
        return
    }
    defer stmt.Close()
    if odbcerr = conn.Commit(); odbcerr != nil {
        err = errors.New(\"提交失败!SQLState: \" + odbcerr.SQLState + \", ErrorMessage: \" + odbcerr.ErrorMessage)
        conn.Rollback()
        return
    }

    return nil
}

下面是一些其它的数据库驱动和库:

不多说,上代码,使用了第三方库osext来获取自身路径。

package main

import (
    \"bitbucket.org/kardianos/osext\"
    \"fmt\"
    \"os\"
    \"path/filepath\"
)

func main() {
    //Go自带的获取当前工作目录的方法,但是工作目录不一定是程序自身所在的目录
    file, _ := os.Getwd()
    fmt.Println(file)
    //Go自带库没有获取可执行文件路径的能力,需要使用第三方库
    var err error
    if file, err = osext.Executable(); err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(file)
    }
    fmt.Println(filepath.Dir(file))    //分离出路径,最后不带\'\'
    file, _ = osext.ExecutableFolder() //分离出路径,最后带\'\'
    fmt.Println(file)
    file, _ = osext.GetExePath() //用法与Executable一样,不过此方法已过时
    fmt.Println(file)
}