作为开发,我们总是需要关心调用一个服务,执行一个性能较差的代码,这个时候我们需要用一些简单的方法将timing logger,然后作为参考,有时候业务很重要的逻辑,我们都需要线上加入这个日志来采集我们需要的timing来衡量代码是否正常.这篇博客简单的介绍下在golang中捎带技巧的方法.

简单的计算方法

func main() {
    start := time.Now()
    //执行比较耗时的计算
    time.Sleep(time.Second * 2)
    elapsed := time.Since(start)
    log.Printf("MAIN函数耗时:", elapsed)
}

按照我们的续期 日志 正常输出:

2019/09/12 16:56:23 MAIN函数耗时: 2.001828676s

当然,这也适用于函数调用,但是很快就会变得混乱。如果我们想把这个技巧应用到代码中的许多部分,会怎么样?然后你可以使用时间跟踪技巧。

利用defer抽象出时间统计的func ????

func TimeTrack(start time.Time, logName string) {
    elapsed := time.Since(start)
    log.Printf("%s 耗时 %s", logName, elapsed)
}

func main() {
    defer TimeTrack(time.Now(), "main")
    time.Sleep(time.Second * 2)
}

output:

2019/09/12 17:00:50 main 耗时 2.001762384s

上面的方法只是一个简单的定制,你完全可以传递更多有用的信息到TimeTrack,将耗时这个日志统计化.

总结:

原本计算耗时的需要将耗时的代码部署在计算的开始和结束,这样当代码量大,或者需要很多处统一,就闲的不够优雅,或者看着很乱,利用defer的特性就可以完全解决问题.这个思路就和我们open一个资源做一堆操作,最后close一样.

另外这里特别有一处细节需要考虑,defer里面传递的time.Now()到底是执行defer语句才执行,还是申明defer就执行呢???

超时是一种常见的并发模式。您希望等待一个长时间运行的任务,但不希望永远等待。有几种方法可以在Go中实现超时,有些方法比其他方法更容易管理。我将概述其中的三种方法(尽管我不建议使用第一个方法),如果您想跳过,我更喜欢第三种方法。

方法一:快而不优雅

第一个方法是我认为大多数人会首先尝试的方法,因为它使用了许多语言中常见的概念,而且它在谷歌搜索“golang timeout”时排名很高,这是2010年的一篇博客文章中概述的。使用time . sleep:

ch := make(chan bool, 1)
timeout := make(chan bool, 1)

// 1s后,往timeout的chan中发送bool值true
go func() {
  time.Sleep(1 * time.Second)
  timeout <- true
}()

// 要么1s拿到ch结果,要么当1s时拿到timeout chan结果(这时候超时)
select {
case <-ch:
  fmt.Println("Read from ch")
case <-timeout:
  fmt.Println("Timed out")
}

这个示例将等待,直到它从ch或超时通道接收到一些内容。因为我们从来没有发送给ch,它总是在1s之后超时。好又简单。然而,事后很难清理干净。如果我们不超时,并试图关闭通道,当超时最终被触发时,我们的代码将会出现panic。

ch := make(chan bool, 1)
timeout := make(chan bool, 1)
defer close(ch)
defer close(timeout)

go func() {
  time.Sleep(1 * time.Second)
  timeout <- true
}()

go func() {
  ch <- true
}()

select {
case <-ch:
  fmt.Println("Read from ch")
case <-timeout:
  fmt.Println("Timed out")
}

错误信息

方法B: 就一行代码

有用的是,time package提供了After功能,它可以为我们创建超时通道:

ch := make(chan bool, 1)
defer close(ch)

go func() {
  ch <- true
}()

select {
case <-ch:
  fmt.Println("Read from ch")
case <-time.After(1 * time.Second):
  fmt.Println("Timed out")
}

由于在select语句之后我们没有保留通道,所以垃圾收集器将在超时之后为我们清理所有东西。对于不需要经常处理超时的长时间运行的应用程序,这应该没有问题。但是在很多情况下,我们想要确保我们把所有的东西都清理干净。

方法C:用时间定时器自己清理

如果你看一下godoc。之后,您可能已经被引导到此选项。引擎盖下是时间。使用定时器结构后,可以根据需要显式停止:

ch := make(chan bool, 1)
defer close(ch)

go func() {
  ch <- true
}()

timer := time.NewTimer(1 * time.Second)
defer timer.Stop()

select {
case <-ch:
  fmt.Println("Read from ch")
case <-timer.C:
  fmt.Println("Timed out")
}

这需要比前一个示例多一点的代码,但是您可以放心,当函数返回时,它所使用的所有通道都已被清除。

方法D:使用context

context提供了withTimeout的方法 ?

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*3))
    defer cancel()
    dataChan := make(chan bool)

    go func(ctx context.Context, dataChan chan bool) {
        select {
        case <-ctx.Done():
            fmt.Println("done")
        case <-dataChan:
            fmt.Println("data parse ok!")
        }

    }(ctx, dataChan)

    go func() {
        time.Sleep(time.Second * 4)
        dataChan <- true
    }()

    time.Sleep(time.Second * 5)

}

总结:

并发编程,路漫漫其修远兮!

GoJSON 标准库 library support 是支持配置文件的一中方式. 应用程序可以定义一个包含所有可配置元素的配置结构类型,并将JSON从一个文件加载到这个结构的实例中,例如:

type Config struct {
    ServerUrl   string
    APIKey        string
    MaxSessions int
}

func readConfig(filename string) (*Config, error) {
    // 实例化一个默认的配置结构体
    conf := &Config{Url: "http://localhost:8080/", MaxSessions: 10}

    b, err := ioutil.ReadFile("./conf.json")
    if err != nil {
        return nil, err
    }
    if err = json.Unmarshal(b, conf); err != nil {
        return nil, err
    }
    return conf, nil
}

这种方式相对于没有类型的数据结构(例如js或者python的数据结构)已经是一种提升,例如我们可以使用json.Unmarshal来实现简单的参数验证,如果配置文件是:

{ "MaxSessions": "20 seconds" }

json.Unmarshal将会return出一个错误提示: "20 seconds" is not a valid integer, 然后我们就可以将这种判断放在启动load文件而不是出现在运行环境中了.

定制

- 阅读剩余部分 -

你知道什么是类工厂吗?为什么它有用?是否考虑过在Golang中实现一个类工厂,即使Go在本质上不是完全面向对象的?除了令人印象深刻的并发功能之外,您是否对Go中的一些出色特性感到好奇?如果你对这些问题中的任何一个感兴趣,那么继续读下去也许是值得的。本文介绍了在Go中实现工厂设计模式。

什么是工厂模式?

目前在网上已经有很多关于工厂模式的定义了,其实工厂模式的概念非常简单.在oop的环境中,工厂或者类工厂是一种最小化的添加未来代码的设计方式,但是为什么我们需要将未来的代码添加到现有的应用程序中呢?因为这是软件行业的本质, 需求永远在变

类工厂通过使用接口和继承来实现面向对象世界中新旧代码之间的解耦。守则将分为三部分:

- 阅读剩余部分 -

在这篇文章中,我将从头梳理PHP开发中 Exception 这个关键字的周边知识,争取梳理清楚如下的几个问题
1.什么是异常
2.异常从哪里来
3.异常应该到哪里去
4.异常之分门别类
5.异常和错误的区别和联系
6.异常之于业务场景

什么是异常

在我们开始所有解释之前,我么首先举个栗子.
假设你要按照给定id来查找humans表的用户邮箱,这个功能可以写成这样:

function getHumanEmailById($id)
{
    return \Humans::whereId($id)->email;
}

但是这绝对不是一个健壮的代码,一旦用户传递一个不存在的id(数据库不存在),或者传递是压根就不是int型的正整数,程序就会进入: PHP Notice 状态,导致整个程序进入非预期流程,这里的非预期流程可能是个php层的警告,也可能是个fatal error(假设方法的实现还有更加致命的逻辑假设),导致程序异常终止,所以这时候需要这个方法告诉调用方你的参数有误这种信息,这个时候通常我们的做法有两种.
1.

if (##如果id非法) {return '';}

2.

if (##如果id非法) {throw new \InvalidArgumentException("参数非法");}

而第二种做法就是我们讨论的Exception,好了,到这里我们只是讨论清楚了抛出异常的必要性,但是就如上面两个例子看到的,调用方调用完毕这个方法怎么按照你给的信号(throw 出来的exception)做出合理的处理呢?

异常从哪里来?

Q:上面说清楚了异常的必要性以及可能性,那么异常从哪里来呢?
A:异常来自开发者对整体代码逻辑的非预期结果给出的提示.
所以简单来说,异常是PHP代码层抛出的.

异常应该到哪里去

上面已经定义了function getHumanEmailById,同时对参数的非法性做了异常的抛出,但是我们不知道异常到哪里去了,我们尝试调用它getHumanEmailById(1),这里假设id是1的human信息在数据库中已经被删除,看看php执行器返回结果:

Fatal error: Uncaught InvalidArgumentException: 参数非法

这里发现,我们抛出的异常居然变成了Fatal error,所以异常抛出后交给了PHP解释器,解释器没有找到 catch 这个异常的逻辑代码,所以直接fatal error,说明这一点,我们继续修改代码

    try{
        getHumanEmailById(1);
    }catch (\Exception $e)
    {
        echo $e->getMessage();
    }

执行结果正常输出: 参数非法
到此,我们解释清楚了异常应该到哪里去的问题

异常之分门别类

也许你留意到上面的代码没有catch InvalidArgumentException,是因为ExceptionInvalidArgumentException的父类,因为异常的类型很多,我们有一些需求确实需要根据不同的异常做不同的事情,例如下面的伪代码:

try {
    $variable = 'string';
} catch (MyException $e) {
    ##todo1
} catch (YourException $e) {
    ##todo2
} catch (OurException $e) {
    ##todo3
} catch (Exception $e) {
    echo "异常信息:" . $e->getMessage();
}

如果你想了解更多的异常分类,可以查看php手册,也可以通过执行 这个脚本 来打印异常分类.

异常和错误的区别和联系

这个主题,我想只有PHP官方php5时代的开发才能解释清楚这一点,通俗的来说,错误可能是不可修复的,当然现在的php7版本已经将error和exception统一了,他们都集成自throwable这个interface.具体的关系如图:
继承关系

php7一下的话,我们可以尝试将error转为exception,具体实现代码:

protected function registerErrorHandler()
{
    set_error_handler(array($this, 'handleError'));
}

public function handleError($level, $message, $file, $line, $context)
{
    if (error_reporting() & $level)
    {
        throw new ErrorException($message, $level, 0, $file, $line);
    }
}

更多是实现方案参照 laravel的exceptionHandler

异常之于业务场景

特定一类throwable统一输出json

最后,我们回归到上面最初的代码,如果human的id是非法的,就抛出了异常,假设这个id恰好是业务前端传递的,我们就需要告诉用户这个id是非法的,明确告诉他非法请求.
实现的逻辑代码大致为:

        try{
            getHumanEmailById(1);
        }catch (\InvalidArgumentException $e)
        {
            echo json_encode([
                'code' => 444, 
                'msg' => "参数id错误: " . $e->getMessage(), 
                'data' => []
            ]);
        }

如果这中参数对应的msg想统一起来,且前端提交的参数非常多,都需要这样判断呢? 这里我们可以抽象出有一个类,继承自InvalidArgumentException的类ApiInvalidArgumentException,然后统一在上层捕获这个异常,然后统一输出json格式
这里的最佳实践可以在laravel中看到影子,大致思路如下:
1.继承IlluminateFoundationExceptionsHandler::render($request, Exception $e)
2.ApiInvalidArgumentException类定义toJson方法
3.拿到$e,特判ApiInvalidArgumentException,return出tojson

如此对业务代码无侵入,看起来干净且明了

将没有catch的异常介入第三方统计

线上在所难免的会有一些异常是未捕获的,这时php会将信息直接fatal error,并输出堆栈信息,所以我们可以将这些信息介入第三方,实时发消息给开发者,解决和发现线上问题.
推荐大家使用sentry作为php异常处理和发现的工具,很强大,目前我们团队线上就在大量使用.

总结

因为我们的业务需要判断和处理太多太多不符合预期的结果了,有时候一个鲁棒性强的代码可能是核心业务代码的2-3倍,这个时候如果我们能够很好的利用exception,既能让代码健壮,又能让健壮性判断可读性高,例如我们可以二次封装if判断,转变为assert断言,然后在断言中抛出异常.现在很多第三方类库,都很好的定义了自己的异常,为的就是健壮和可读性,希望通过这篇文章,大家能够收获一些新的心得体会,也希望你能斧正文章的错误,下篇博客见!