Go 实现简单的 List 链表


需求
大家都知道基本链表得有以下特性:链表的初始化、链表的长度、节点的插入、删除、查找等一些常见的基本操作,最后写好之后,需要测试。

实现
初始化
有语言基础的人都知道,链表是由节点连接而成,这其中在定义一个 List 数据结构之外,还需要定义一个 Node 类型的数据结构。先说 Node 类型的数据结构,首先 List 按照正常的设计应该是可以存储基本类型的数据的,这就要求 Node 中的 Value
的类型不能固定。

type Node struct {
    Value      interface{} 
    next, prev *Node       
}

下面就是定义 List 结构体了,有了上面的分析,List 结构体的定义就很好实现了:

type List struct {
    // 头节点
    root   Node
    // list 长度
    length int
}

处理好空 List:

// 返回 List 的指针
func New() *List {
    // 获取 List{} 的地址
    l := &List{}
    // list 初始长度为0
    l.length = 0
    l.root.next = &l.root
    l.root.prev = &l.root
    return l
}

判空和长度
List 的判空和获取长度也是非常基础和重要的,判断是否为空,返回的数据类型是布尔类型的。什么情况下 List 是为空呢?根据前面的定义,头节点的 next 指针域指向是头结点本身的地址即为空。另外,判空函数写好了,总不能什么类型的数据都去调用这个函数,我们需要指定调用的数据类型,在本例中当然是 List 类型的了,为了方便操作,传入一个 List 的地址即可。

func (l *List) IsEmpty() bool {
    return l.root.next == &l.root
}

分析完毕之后,获取 list 的长度就简单很多了:

func (l *List) Length() int {
    return l.length
}

头插和尾插
因为在定义 List 数据结构的时候,就定义了一个 root 头节点。所以此时,可以很方便的实现头插入和尾插入。考虑能够同时插入多个 Node 节点,利用 Go 中的变长参数实现该特性。对插入的 Node 节点进行循环处理,新节点的指针域和 root 节点的指针域做相应改变:

func (l *List) PushFront(elements ...interface{}) {
    for _, element := range elements {
        n := &Node{Value: element}
        // 新节点的 next 是 root 节点的 next
        n.next = l.root.next
        // 新节点的 prev 存储的是 root 的地址
        n.prev = &l.root
        // 原来 root 节点的 next 的 prev 是新节点
        l.root.next.prev = n
        // 头插法 root 之后始终是新节点
        l.root.next = n
        // list 长度加1
        l.length++
    }
}

尾插法:

func (l *List) PushBack(elements ...interface{}) {
    for _, element := range elements {
        n := &Node{Value: element}
        n.next = &l.root
        n.prev = l.root.prev
        l.root.prev.next = n
        l.root.prev = n
        l.length++
    }
}

查找
查找最终的效果是返回指定数值的索引,如果不存在的话返回-1即可。对于链表的查找是一个遍历的过程,在此时就需要考虑遍历的起始和终止区间了,不能越界出错。因为是循环链表,终止节点也很好办。

func (l *List) Find(element interface{}) int {
    index := 0
    p := l.root.next
    for p != &l.root && p.Value != element {
        p = p.next
        index++
    }
    // p 不是 root
    if p != &l.root {
        return index
    }
    return -1
}

删除
链表的删除操作逻辑很清晰,将一个 Node 的节点与前后节点断开即可,同时前后节点和 Node 节点本身指针域也要做相应修改,最后别忘记将链表的长度减少相应长度。

func (l *List) remove(n *Node) {
    n.prev.next = n.next
    n.next.prev = n.prev
    n.next = nil
    n.prev = nil
    l.length--
}

删除并返回 List 中的第一个数据:


func (l *List) Lpop() interface{} {
    if l.length == 0 {
        // null 的表现形式 nil
        return nil
    }
    n := l.root.next
    l.remove(n)
    return n.Value
}

遍历
下面 normalIndex 函数的作用返回一个正常逻辑的 Index,例如处理好一些越界问题:

func (l *List) normalIndex(index int) int {
    if index > l.length-1 {
        index = l.length - 1
    }
    if index < -l.length {
        index = 0
    }
    // 将给定的 index 与 length 做取余处理
    index = (l.length + index) % l.length
    return index
}

如下的函数为获取指定范围内的数据,根据传入的参数需要指定 start 和 end,最后返回的应该是一个切片或者数组,具体类型未知:

func (l *List) Range(start, end int) []interface{} {
    // 获取正常的 start 和 end
    start = l.normalIndex(start)
    end = l.normalIndex(end)
    // 声明一个 interface 类型的数组
    res := []interface{}{}
      // 如果上下界不符合逻辑,返回空 res
    if start > end {
        return res
    }
    sNode := l.index(start)
    eNode := l.index(end)
    // 起始点和重点遍历
    for n := sNode; n != eNode; {
          // res的append方式
        res = append(res, n.Value)
        n = n.next
    }
    res = append(res, eNode.Value)
    return res
}
阅读全文

Go 语言小细节(三)


在 for 语句的闭包中使用迭代变量会有问题
在 for 迭代过程中,迭代变量会一直保留,只是每次迭代值不一样。因此在 for 循环中在闭包里直接引用迭代变量,在执行时直接取迭代变量的值,而不是闭包所在迭代的变量值。如果闭包要取所在迭代变量的值,就需要 for 中定义一个变量来保存所在迭代的值,或者通过闭包函数传参。

package main

import (  
    "fmt"
    "time"
)

func forState1(){
    data := []string{"one","two","three"}

    for _,v := range data {
        go func() {
            fmt.Println(v)
        }()
    }
    time.Sleep(3 * time.Second)
    // three, three, three

    for _,v := range data {
        vcopy := v
        // 使用临时变量
        go func() {
            fmt.Println(vcopy)
        }()
    }
    time.Sleep(3 * time.Second)
    // one, two, three

    for _,v := range data {
        go func(in string) {
            fmt.Println(in)
        }(v)
    }
    time.Sleep(3 * time.Second)
    // one, two, three
}

func main() {  
    forState1()
}

阅读全文

Go 代码规范


项目目录结构规范
PROJECT_NAME
├── README.md 介绍软件及文档入口
├── bin 编译好的二进制文件
├── doc 该项目的文档
└── src 该项目的源代码

├── main 项目主函数
├── controller 路由控制
├── module 模块
└── conf 配置文件

包名
包名用小写,使用短命名,尽量和标准库不要冲突。

接口名
单个函数的接口名以”er”作为后缀,如 Reader、Writer

接口的实现则去掉“er”

type Reader interface {
    Read(p []byte) (n int, err error)
}







阅读全文

Go 操作 excel


读 excel

package main
    
import (
    "fmt"
    
    "github.com/tealeg/xlsx"
)
    
func main() {
    excelFileName := "test.xls"
    xlFile, _ := xlsx.OpenFile(excelFileName)
    for _, sheet := range xlFile.Sheets {
        for _, row := range sheet.Rows {
            for _, cell := range row.Cells {
                fmt.Println(cell.String())
            }
        }
    }
}

写 excel

package main
    
import (
    "encoding/csv"
    "os"
)
    
func main() {
    f, err := os.Create("test.xls")
    if err != nil {
        panic(err)
    }
    defer f.Close()
    // 写入 utf-8 编码
    f.WriteString("\xEF\xBB\xBF")
    w := csv.NewWriter(f)
    w.Write([]string{"编号", "姓名", "年龄"})
    w.Write([]string{"1", "张三", "23"})
    w.Write([]string{"2", "李四", "24"})
    w.Write([]string{"3", "王五", "25"})
    w.Write([]string{"4", "赵六", "26"})
    w.Flush()
}
阅读全文

Go 处理时间


package main

import (
    "fmt"
"strconv"
    "time"
)

// TimeUnix is time for unix
func TimeUnix() string {
naiveTime := time.Now().Unix()
naiveTimeString := strconv.FormatInt(naiveTime, 10)
return naiveTimeString
}

func main() {
    timeNow := time.Now()
    fmt.Println("tNow(time format): ", timeNow)
    
    // 时间转化为 string layout 必须为 "2006-01-02 15:04:05"
    timeNowStr := timeNow.Format("2006-01-02 15:04:05")
    fmt.Println("tNow(string format): ", timeNowStr)

    timeNowUnixInt := timeNow.Unix()
    fmt.Println(timeNowUnixInt)

    // string 转化为时间 layout 必须为 "2006-01-02 15:04:05"
    t1, _ := time.Parse("2006-01-02 15:04:05", "2014-06-15 08:37:18")
    fmt.Println("t(time format): ", t1)
    t2 := time.Unix(1389058332, 0).Format("2006-01-02 15:04:05")
    fmt.Println("t(time format): ", t2)
}
阅读全文

Go Error 处理


Go 中关于错误的哲学,应该尽可能优雅地处理错误。

先说结论:

  • 使用"隐藏内部细节的错误处理"
  • 使用 errors.Wrap 封装原始 error
  • 使用 errors.Cause 找出原始 error
  • 为了行为而断言,而不是类型
  • 尽量减少错误值的使用

error 的类型是 interface

type error interface {
    Error() string
}

创建error:

// example 1
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

// example 2
if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

阅读全文

Go 使用 OS 包操作文件


判断文件是否存在 存在返回 true 不存在返回 false

func checkFileIsExist(filename string) bool {
    var exist = true
    if _, err := os.Stat(filename); os.IsNotExist(err) {
        exist = false
    }
    return exist
}

打开文件 返回文件指针

file, error := os.Open("/tmp/1.txt")
if error != nil {
    fmt.Println(error)
}
fmt.Println(file)
file.Close()

以读写方式打开文件 返回文件指针 如果不存在则创建

file2, error := os.OpenFile("/tmp/2.txt", os.O_RDWR|os.O_CREATE, 0644)
if error != nil {
    fmt.Println(error)
}
fmt.Println(file2)
file2.Close()

创建文件 Create 函数也是调用的 OpenFile

file3, error := os.Create("/tmp/3.txt")
if error != nil {
    fmt.Println(error)
}
fmt.Println(file3)
file3.Close()

删除文件

del := os.Remove("/tmp/1.txt")
if del != nil {
    fmt.Println(del)
}

删除指定 path 下的所有文件

delDir := os.RemoveAll("/tmp/testdir")
if delDir != nil {
        fmt.Println(delDir)
}

阅读全文

Go 使用 clientv3 连接 etcd


etcd3 和 etcd2 的 api 不太一样。
etcd3 移除了目录概念,只有单纯的 kv。
api 也只有 rpc 模式,http 需要 proxy。

package etcd

import (
    "context"
    "log"
    "time"

    "../conf"

    "go.etcd.io/etcd/clientv3"
)

var (
    endpoints   = make([]string, 1)
    dialTimeout = 3 * time.Second
    username    string
    password    string
)

// Init is init etcd
func Init() {
    endpoints = append(endpoints, conf.RunTimeInfo.EtcdHost)
    username = conf.RunTimeInfo.EtcdUser
    password = conf.RunTimeInfo.EtcdPasswd
}

// connetEtcd is connect etcd
func connetEtcd() *clientv3.Client {
    cli, cliErr := clientv3.New(clientv3.Config{
        Endpoints:   endpoints,
        DialTimeout: dialTimeout,
        Username:    username,
        Password:    password,
    })
    if cliErr != nil {
        log.Println(cliErr)
    }
    return cli
}

// KeepAlive is make etcd keepalive with ttl
func KeepAlive(keyID clientv3.LeaseID) {
    cli := connetEtcd()
    if cli != nil {
        ka, kaErr := cli.KeepAliveOnce(context.TODO(), keyID)
        if kaErr != nil {
            log.Println(kaErr)
        }
        log.Println("ttl:", ka)
        cli.Close()
    }
}

// GetKV is get kv from etcd
func GetKV(dir string) []string {
    cli := connetEtcd()
    var kvList = make([]string, 0)
    if cli != nil {
        resp, respErr := cli.Get(context.TODO(), dir, clientv3.WithPrefix())
        if respErr != nil {
            log.Println(respErr)
        }
        for _, ev := range resp.Kvs {
            kvTemp := string(ev.Key) + ":" + string(ev.Value)
            kvList = append(kvList, kvTemp)
        }
        cli.Close()
    }
    return kvList
}

// PutKV is set kv to etcd
func PutKV(writePath string, writeValue string) {
    cli := connetEtcd()
    if cli != nil {
        resp, respErr := cli.Put(context.TODO(), writePath, writeValue)
        if respErr != nil {
            log.Println(respErr)
        }
        if resp != nil {
            log.Println(resp.Header)
            cli.Close()
        }
    }
}

// PutKVTTL is set kv to etcd with ttl
func PutKVTTL(registKey, registValue string) {
    cli := connetEtcd()
    if cli != nil {
        resp, respErr := cli.Grant(context.TODO(), 10)
        if respErr != nil {
            log.Println(respErr)
        }
        conf.ServID = resp.ID
        putResp, putErr := cli.Put(context.TODO(), registKey, registValue, clientv3.WithLease(resp.ID))
        if putErr != nil {
            log.Println(putErr)
        }
        log.Println(putResp.Header)
        cli.Close()
    }
}

// DelKV is del kv to etcd
func DelKV(writePath string) {
    cli := connetEtcd()
    if cli != nil {
        resp, respErr := cli.Delete(context.TODO(), writePath)
        if respErr != nil {
            log.Println(respErr)
        }
        log.Println(resp.Header)
        cli.Close()
    }
}
阅读全文

Go convert


[]string to string

package main

import (
    "fmt"
    "strings"
)

var naiveList string

func main() {
    naiveArray := []string{"a", "b", "c", "d", "e"}
    for _, v := range naiveArray {
        if naiveList == "" {
        naiveList = v
        } else {
        naiveList = strings.Join([]string{naiveList, v}, ",")
        }
    }
    fmt.Println(naiveList)
}

map to struct

import (
    "testing"
    "github.com/goinggo/mapstructure"
)

func TestMap2Struct(t *testing.T) {
    mapInstance := make(map[string]interface{})
    mapInstance["Name"] = "liang637210"
    mapInstance["Age"] = 28
    var person Person
    // 将 map 转换为指定的结构体
    if err := mapstructure.Decode(mapInstance, &person); err != nil {
        t.Fatal(err)
    }
    t.Logf("map2struct 后得到的 struct 内容为:%v", person)
}

struct to map

import (
    "testing"
    "reflect"
)

type User struct {
    Id        int    `json:"id"`
    Username    string    `json:"username"`
    Password    string    `json:"password"`
}

func Struct2Map(obj interface{}) map[string]interface{} {
    t := reflect.TypeOf(obj)
    v := reflect.ValueOf(obj)
    var data = make(map[string]interface{})
    for i := 0; i < t.NumField(); i++ {
        data[t.Field(i).Name] = v.Field(i).Interface()
    }
    return data
}

func TestStruct2Map(t *testing.T) {
    user := User{5, "zhangsan", "password"}
    data := Struct2Map(user)
    t.Logf("struct2map 得到的 map 内容为:%v", data)
}
阅读全文