战魂小筑

讨论群:309800774 知乎关注:http://zhihu.com/people/sunicdavy 开源项目:https://github.com/davyxu

   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  251 随笔 :: 0 文章 :: 506 评论 :: 0 Trackbacks

2018年8月29日 #

为了编写基于cellnet的新一代游戏服务器框架,最近深入研究微服务,ServiceMesh等概念。研究过程中对Web和游戏两种服务器架构设计有一些心得,编写并记录下来。(下文中,Game表示游戏服务器,Web表示Web服务器) ``
状态缓存
所谓状态缓存,就是在内存而非专业数据缓存服务器(如redis)中保存和处理逻辑数据,手动编写此过程较为繁琐但是效率较高,但随着状态逻辑复杂性和并发、扩容问题提出,状态同步会变得越来越复杂。
Game:
强交互性的服务器类型需要在服务器做缓存,逻辑编写也较为容易,无需处理事务并发问题。例如:组队,匹配,战斗逻辑。服务器不能随意重启。
弱交互性的服务器类型可配合redis做成无状态服务器,例如:养成,技能升级,领取物品等。服务器随时支持重启。
游戏服务器为了提高性能,早期所有服务器都是使用状态缓存写法编写,特别是MMORPG这类强交互的游戏服务器尤为严重。
Web:
均为无状态服务器,弱交互。使用事务方式处理并发逻辑,例如:交易,下单等。
推送,单独发送
这里提到的所谓推送,单独发送是与RPC区别的通讯方法。RPC要求请求必须有回应。而推送单独发送则更像是通知和广播,无需目的方返回任何消息。
Game:
找到服务器的Session,直接Send
通过中转服务器,或称为中心服务器进行注册/广播
客户端的model数据需要更新时,服务器会主动推送消息。
游戏服务器没有严格的RPC设计需求,推送和单独发送较Web服务器更多。而且游戏服务器多使用长连接,所以主动推送也比Web服务器来的方便一些。
Web:
将推送做成专有的服务,并做排队和并发处理。
可用性
听说过游戏停服更新,支付宝服务器在刷二维码时停服了可一定被骂惨吧。Web对服务器高可用性要求很高,游戏虽然也注重服务器稳定性和可用性,但是由于版本迭代更新频繁,停服更新反而能获得玩家接受。
Game:
游戏对可用性要求不高。
游戏大版本更新时需要停服更新。支持热更新技术的服务器(例如Erlang,Skynet)仅使用热更新修复bug,很少直接更新新版本。
不是所有的游戏服务器支持动态添加服务器。
Web:
极高的可用性,服务不允许停服更新,使用蓝绿及灰度方式更新服务器。
随时可以横向扩展服务器,提高服务器容量和承载。
连接及传输
均使用TCP传输协议,游戏服务器注重性能,自有协议及二进制协议使用较多。
Web注重兼容和接口友好,使用JSON格式较多。
Game:
使用长连接,需要从逻辑层维护连接状态及处理服务器不在线情况
使用自有封包格式,大部分使用protobuf或二进制流格式。
Web:
微服务大部分使用短连接,grpc支持http2长连接
使用json编码方便调试和版本兼容。
流量限制
人数多了,任何服务器都扛不住,流量限制和登入限制能有效保护服务器稳定。
Game:
单服有人数限制,可以通过GM后台设置挡墙,超过无法进入
Web:
限流器中间件,可以精确到服务控制流量
断流,防止雪崩
Game:
游戏没有,也不需要这种概念,游戏请求不会突然升高,即便有,也通过GM后台人为控制
Web:
断流器中间件
服务发现
如何找到服务器地址。
服务有变化时,通过Watch系统通知订阅者更新本地缓存
服务器没有变化时,使用本地缓存找到服务地址
Game:
游戏服务器互相依赖复用只在很小的范围内,因此无需在不同语言不同进程服务间获得地址,大部分在配置文件中填写各服务的IP及地址即可互相访问。
早期游戏自己编写服务器状态及地址发现服务。
有用redis做服务发现
Web:
使用服务发现系统,分布式部署。无需依赖配置文件
网关需求
Game:
网关处理客户端上下线通知,心跳,维持连接,转发,广播上下行封包
Web:
根据请求地址路由,无上下线概念,无心跳。广播通过消息推送系统完成
由于笔者从事游戏行业,对Web服务器概念在逐渐熟悉中,若有错误和不足请各位大佬指出。
本人新书《Go语言从入门到进阶实战》,生动的语言,例子带有各种彩蛋,轻松了解Go语言特性,更有cellnet框架剖析解密
https://search.jd.com/Search?keyword=go%E8%AF%AD%E8%A8%80%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E8%BF%9B%E9%98%B6%E5%AE%9E%E6%88%98&enc=utf-8&suggest=1.def.0.V02&wq=Go%E8%AF%AD%E8%A8%80%E4%BB%8E&pvid=145d55a92cab4b07b71326f8beb1700b
posted @ 2018-08-29 11:16 战魂小筑 阅读(594) | 评论 (0)编辑 收藏

2017年12月19日 #

嵌套git库的管理
使用git作为源代码管理时,经常需要在一个git代码库中从外网获取新的git库,也就是在git库下嵌套另外一个git库。而同时维护两个git库的过程就变的非常复杂。
submodule的弊端
常见的做法是使用git 提供的submodule功能。但submodule的管理嵌套git库的结果往往不是我们期望的结果。假设有一个git库叫project,在project的某个子目录下还包含一个叫3rd的目录,是另外一个git库。
D:.
└─project
└─3rd
mytext.txt
假设甲和乙都取了project的代码,由于时间差异,甲通过submodule取到V1版,而乙取到了V2版本。甲乙同时在不同的3rd库下进行开发,势必会造成不同的运行结果。也许你认为可以在获取submodule时指定版本,但这个获取过程很难控制。
一般说来,第三方库应由主程序进行更新及维护,一般情况下,项目没有特殊需求时,不会随便更新第三方库到最新版本,因此submodule更新嵌套的git库并不是理想的解决方案。
嵌套git库的修改可见性
前面的例子中,project git库下的3rd的git库中如果有文件发生修改,此时在project目录下,使用sourceTree等git管理工具无法识别3rd下的文件修改。
也就是说,无法将3rd下的修改提交到project的git库中。
终极解决方案
此时,将3rd目录下的.git目录暂时移出project和3rd目录,此时,在project目录用sourceTree查看时,将可以看到3rd下的文件修改,提交修改到project库中。再将刚移出去的3rd的.git目录移回3rd目录下,在3rd目录下使用git status,将可看到3rd目录的修改,提交时,会将修改提交到3rd目录。
posted @ 2017-12-19 22:04 战魂小筑 阅读(895) | 评论 (0)编辑 收藏

2017年11月27日 #

越来越多的人选择用Mac或者Linux环境进行跨平台项目开发。但是仍然有大部分人习惯于在Windows环境下进行开发,毕竟Windows在各方面使用还是较为方便,特别像文件版本管理(Git,SVN等)

在跨平台下开发游戏或软件,就需要有一套方便的自动化工具。Windows下需要使用批处理,虽然有PowerShell加持,但这东西学了也不靠谱,只有一个平台能用。大家还是习惯Linux Shell。连Mac平台都可以用Shell,Windows上要做自动化脚本就显得非常尴尬。

我曾经在项目中使用go编写了一套将配置转为批处理和Linux Shell的工具。使用过程较为复杂,但是能跨平台进行表格导出和协议编译等工作。

但是,这个工具还是需要对不同的平台编写多套模板进行代码生成,非常繁琐。如果有一套跨平台的Shell,编写一次就可以跨平台运行那该多好。

查阅资料后,一共有几个方案:

  1. 使用Python作为自动化工具 这个方案其实就是使用python把批处理和Shell干的事情用代码来解决。但前提是要重新学习Python,也需要一部分熟悉简单的Python语法,人为学习成本较高,也比较费事。

  2. 自己编写一套独立的自动化工具 这个方案需要长时间的适配过程,差什么指令补什么指令,对项目进度有一定干扰。

  3. 自己编写Linux Shell的解释器 这个就更难了,要做到100%兼容,基本不可能。

  1. 使用Cygwin和Mingw 需要一个微型运行时进行Linux Shell的解释,msys大概是18M左右,可行性较高。

在研究Cygwin和Mingw如何整合的过程中,我误操作点击了一个sh后缀的Linux Shell,这是我希望让Mingw运行的Shell。结果呢,sh后缀的文件居然能在Windows下运行。我马上编写了一系列的例子,发现几乎完全兼容常用的Shell指令。 经过研究,我发现Windows下能运行sh文件是由Git自带的msys2提供的。MSYS2 (Minimal SYStem 2, http://www.msys2.org/) 是一个MSYS的独立改写版本,主要用于 shell 命令行开发环境。同时它也是一个在Cygwin (POSIX 兼容性层) 和 MinGW-w64(从"MinGW-生成")基础上产生的,追求更好的互操作性的 Windows 软件。

那就是说,只要安装了Git(https://git-scm.com/),那么就可以在Windows下直接运行Linux Shell,只需要将文件后缀命名为sh即可

问过周边友人是否知道这一功能,都说知道,只是没有广播而已,害我研究很久……

posted @ 2017-11-27 15:15 战魂小筑 阅读(2136) | 评论 (3)编辑 收藏

2017年7月6日 #

本文主要研究游戏服务器带状态的热更新需求 http的无状态热更新需求已经有成熟方案, 故不在本文描述范围

基本概念

  • Golang的热更新采用什么机制?

    使用go1.8提供的plugin包机制实现

  • plugin包本身设计的目的是热更新么?

    plugin包其实只是支持将代码分别编译为多个动态库,动态加载后运行 并不能完全支持类似C/C++的动态库方式处理代码

  • 带状态的进程热更新的基本概念及范围是什么?

    数据部分(model)不更新, 只更新逻辑部分(函数)

  • 表格和配置更新算热更新么?

    算, 但不是本文描述范围

  • 热更新能在windows上使用么?

    不支持

代码及结构

  • 我能将原来一个exe的代码编译为so提供给plugin使用么?

    可以, 但是必须仍然保留main包作为插件入口, 并在main包内添加提供给plugin调用入口.

  • 如果动态库中没有main包, 编译出的so能用么?

    不能, 包必须包含main, 否则输出的是.a的文件, plugin包加载会报错

  • 动态库中, 非main包的的代码修改能做热更新么?

    不能!(崩溃了吧, 我提了一个issue: https://github.com/golang/go/issues/20554)

    如果确实做了修改, 底层会报错: plugin was built with a different version of package

    解决方法: 修改plugin包底层实现并重新编译 打开runtime/plugin.go, 注释以下代码 for _, pkghash := range md.pkghashes { if pkghash.linktimehash != *pkghash.runtimehash { return "", nil, pkghash.modulename } } 执行/usr/local/go/run.bash 重编译+测试

  • 代码中哪些可以被更新? 方法可以被更新么? 闭包呢?

    只能更新拥有静态地址的结构.例如: 包级别函数(类似于静态函数)

    例如: svc_0.so里有一个Foo函数, svc_1.so修改了Foo函数实现, 热更新可实现

    闭包=函数+捕获变量, 实际上是一个动态结构, 没有静态地址, 无法被更新

    各种包级别变量, 结构体变量, 结构体方法, 局部变量均不能被热更新, 但是变量值不会被影响

    新增结构可以被运行

  • 使用结构体方法调用了包级别函数, 包级别函数能被更新么?

    可以, 虽然方法不能被更新, 但方法被调用的包级别函数的地址是固定的, 所以可以被热更新

  • init包初始化函数能用么? 能被热更新么?

    官方这样描述:

    When a plugin is first opened, the init functions of all packages not already part of the program are called. The main function is not run. A plugin is only initialized once, and cannot be closed

    插件第一次被打开时, 其关联的, 没有成为程序的一部分的所有的包的init函数将被调用. 插件的main函数不会被调用. 插件只会被初始化一次, 不能被关闭

    因此, 需要手动将init函数改成自己的函数, 统一在so的main包里调用

编译

  • 如何编译获得plugin包支持的动态库

    SVCNAME=$1 SVCVER=$2 TIMESTAMP=`date '+%Y%m%d_%H%M%S'` go build -v -buildmode=plugin --ldflags="-pluginpath=${SVCNAME}_${TIMESTAMP}" -o ${SVCNAME}_${SVCVER}.so ${SVCNAME}

    -buildmode=plugin是重要参数

    --ldflags里的-pluginpath的作用是: 每次编译的内部识别路径都是不同的, 避免重复加载的警告

    参考: https://github.com/golang/go/issues/19004

posted @ 2017-07-06 12:47 战魂小筑 阅读(4666) | 评论 (0)编辑 收藏

2017年4月20日 #

使用Visual Studio Code调试Golang工程

关键字

  • 最简单的调试攻略
  • 多项目调试, 适用个人开发和项目开发
  • 无需修改系统环境变量

准备VSCode

在官网下载最新版的VSCode:

https://code.visualstudio.com/

安装Golang插件

  • 打开扩展面板

    VSCode->查看->扩展

  • 找到Go插件 在搜索框里输入Go, 找到第二行写有 Rich Go language support for Visual Studio Code的插件, 点击安装

    注意不是排名最高的

  • 重启编辑器

配置启动项

  • 打开调试面板

    VSCode->查看->调试

  • 添加调试目标

    在"没有调试"的下拉框中点击"添加配置.."

  • 添加目标调试配置

    例子:

    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "Launch",
                "type": "go",
                "request": "launch",
                "mode": "debug",
                "remotePath": "",
                "port": 2345,
                "host": "127.0.0.1",
                "program": "${fileDirname}",
                "env": {
                    "GOPATH":"D:/Develop/vscodegolang"
                },
                "args": [],
                "showLog": true
            }
        ]
    }

其中: "port", "host"都是go插件自动生成的

"env"为设置环境变量, 设置为你的工程目录就可以(包含bin, src的文件夹)

准备调试插件

此时找到main.go按F5, 会报错提示:

Failded to continue:"Cannot find Delve debugger. Install from https://github.com/derekparker/delve & ensure it is in your "GOPATH/bin" or "PATH"

我们使用go命令行编译调试器

go get github.com/derekparker/delve/cmd/dlv

将dlv调试器放在GOPATH(工程目录)的bin目录下

开始调试

选中要调试的main.go, 点击F5, 既可以开始调试

调试快捷键和Visual Studio系一致

  • F9 切换断点
  • F10 Step over
  • F11 Step in
  • Shift+F11 Step out

注意点

  • 某些结构体成员无法直接显示时, 可以直接选中变量名, 添加到监视, 或者右键点击: "调试:求值"

多项目调试

在launch.json中可以添加多组调试入口, 通过调试面板中选中对应的配置开启不同目标的调试

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "client",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "remotePath": "",
            "port": 2345,
            "host": "127.0.0.1",
            "program": "${fileDirname}",
            "env": {
                "GOPATH":"D:/Develop/vscodegolang"
            },
            "args": [],
            "showLog": true
        },

        {
            "name": "server",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "remotePath": "",
            "port": 2345,
            "host": "127.0.0.1",
            "program": "${workspaceRoot}/src/server",
            "env": {
                "GOPATH":"D:/Develop/vscodegolang"
            },
            "args": [],
            "showLog": true
        }
    ]
}

"program"中的"${fileDirname}"是以当前选中文件作为启动点

更建议使用"program"的"${workspaceRoot}", 以包名作为启动点的方式进行配置

参考链接

https://code.visualstudio.com/Docs/editor/debugging

posted @ 2017-04-20 12:52 战魂小筑 阅读(5849) | 评论 (0)编辑 收藏

2017年4月8日 #

相位技术

相位技术大规模出现在魔兽世界WLK版本, 现在应用已经广泛应用在各种MMORPG游戏中. 下面对相位技术的做法进行简单归纳汇总

表现分类

副本相位

早期副本的出现, 避免抢怪问题. 所以, 副本其实本身就是一种相位技术. 只不过实现时, 我们一般会将小队和怪物直接预分配在独立的一个副本实例中(所以副本原文也是实例的意思)

分线相位

相位技术还没有正式命名时, 同一个场景, 玩家进到不同的分线看到的玩家不一样, 也是属于相位的一种. 当然, 如果是组队玩家, 服务器默认会分配所有队伍玩家在同一个线(位面)

真相位

副本相位和分线相位其实都是静态相位, 一旦进入, 中途不会有切换或者混合查看的过程. 真相位可以在一个场景中,动态切换相位, 相位内和相位外所以不同

我们常见的真相位表现为: 相位中的角色+玩家+队伍成员

在护送任务时, 还会在上面所见角色中叠加世界中的所有角色, 也就是说, 你和队伍成员可以看到的角色, 其他人看不到, 其他人也看不到你和你的队伍成员


为了清晰的简单的描述, 我为相位创建如下概念与名词

相位客体

表现为除玩家外的角色(怪物,交互物体与相位可见场景)

私有客体

这是最常见的一种相位内角色

  • 持有变量

    取PhasingID时,为PhasingTargetID

  • 生成规则

    当玩家开启相位后, 在玩家相位内生成的角色为私有客体.

    此时, 将 PhasingTargetID设置为相位生成者的实例ID

  • 删除规则

    如果玩家退出相位, 私有客体会存在一段时间或按照需求删除

公共客体

一般指提前放置在场景中, 世界内不可见, 但是能被同相位玩家可见,且同相位玩家都可以互相可见 比如: 只要接了同一个任务的玩家, 都可以看到的NPC

  • 持有变量

    取PhasingID时,为PublicPhasingID

  • 生成规则

    通过场景编辑器, 放置角色时, 设置其可被观察到的任务ID

    角色被加载后, 将任务ID设置到StaticPhasingID

  • 删除规则

    场景删除, 角色删除

相位主体

包含玩家与同队伍玩家

  • 开启相位后, 可见私有客体+公有客体

  • 队长视为相位主体, 单人时, 自己为队长

  • 队伍其他成员共享队长的私有相位客体

  • 队伍其他成员根据自己的PublicPhasingID匹配目标对象的PublicPhasingID时可互相可见

  • 持有变量

相位开启时, 取PhasingID时, 为角色实例ID

相位关闭时, 取PhasingID时, 为0

PublicPhasingID

可见规则

当两个角色的PhasingID相等时, 主体与私有客体互相可见

当两个角色的PublicPhasingID相等时, 主体与公有客体互相可见

可以通过开关设置, 是否在可见的相位客体基础上, 叠加世界角色(护送任务)

约束

  • 玩家同时只能看见1个相位
posted @ 2017-04-08 14:41 战魂小筑 阅读(1527) | 评论 (0)编辑 收藏

2016年12月1日 #

Golang中没有设计构造函数. 取而代之的, 设计Golang的大师希望你用普通函数去实现构造的任务. 
一直只是觉得这只是体现Golang这门新语言的精简设计之道, 直到自己实现编译器后才发现构造函数的设计本身是值得商榷的

我们先看下构造函数的规则

构造函数调用规则

构造参数量: 0表示没有构造函数, 1表示有构造函数0个参数

本类构造父类构造处理方法
00不处理
10调本类ctor
01调父类ctor
11调本类ctor, 本类ctor调父类ctor
21调本类ctor, 本类ctor调父类ctor
12报错, 手动调父类ctor
22报错, 手动调父类ctor

普通函数重载规则

实际只用考虑最典型的一种行为: 实例化子类, 转为父类调用方法, 这个时候

如果方法是override, 调用的是子类

如果方法是virutal或者不指明, 调用的是父类

整个重载过程, 子类绝对不会隐式调用父类的行为

需要构造函数么?

构造函数的优点

  • 本身属于一种特殊的成员函数
  • 编译器帮你自动传导调用父级

构造函数的缺点

  • 隐式的调用规则
  • 虽然属于成员函数, 但是与其他成员函数调用规则完全不同, 需要特殊记忆
  • 带参数的构造函数, 在父类参数多于子类时, 需要引用复杂语法来实现父级构造调用

其实我们对初始化函数的需求只有1条: 自定义

所以, 可以理解Golang不加入构造函数的设计是正确的 
即: 简单, 清晰, 有规律

posted @ 2016-12-01 10:45 战魂小筑 阅读(2140) | 评论 (0)编辑 收藏

2016年11月2日 #

append, map, len不是关键字

他们其实还是类库功能, 都在buildin包里的, 系统默认给你做了个

  1. import(
  2. . "buildin"
  3. )

将buildin的包内容都映射到全局而已, 其实你也可以用自己的包这么做

打印的另一种写法

想跟脚本一样调试打印数据么?

  1. println("hello world")

无需包含任何包, 因为它在buildin包里

iota不是黑科技

这是在buildin包里的定义

  1. // iota is a predeclared identifier representing the untyped integer ordinal
  2. // number of the current const specification in a (usually parenthesized)
  3. // const declaration. It is zero-indexed.
  4. const iota = 0 // Untyped int.

其实go是有泛型概念的

想想map和数组的定义 
只是泛型没有开放给用户用而已(只许XX放火,不许XX点灯)

map是支持多个key的, 而且很方便

还在为多个key转id的复杂算法而头疼么?

  1. type myKey struct{
  2. number int
  3. str string
  4. }
  5. func main(){
  6. t := map[ myKey] int {
  7. myKey{ 2, "world"}: 1,
  8. }
  9. fmt.Println(t[myKey{2, "world"}])
  10. }
  11. 输出: 1

枚举是可以转成string的

默认定义一个枚举

  1. type MyConst int
  2. const (
  3. MyConst_A MyConst = iota
  4. MyConst_B MyConst = iota
  5. )
  6. func main(){
  7. fmt.Println(MyConst_A)
  8. }

输出: 0 
如果我们想自动化输出MyConst_A字符串时 
就需要使用golang的一个工具链:golang.org/x/tools/cmd/stringer 
将其下载, 编译成可执行工具后, 对代码进行生成 
生成的代码会多出一个xx_string.go 
里面就是枚举的String()string 函数

临时转换一个接口并调用的方法

  1. type love struct{
  2. }
  3. func (self*love)foo(){
  4. fmt.Println("love")
  5. }
  6. func main(){
  7. var chaos interface{} = new(love)
  8. chaos.(interface{
  9. foo()
  10. }).foo()
  11. }

Golang的receiver实际上就是this的变种实现

  1. func( self*MyStruct) foo( p int ){
  2. }

写不惯receiver的写法? 如果这样改下呢?

  1. func foo( self *MyStruct, p int ){
  2. }

所以为什么说Golang还是一个C语言嘛

关于内存分配…

  • new 传入Type类型, 返回*Type类型
  • make 可以在分配数组时设置预分配大小, new不可以
  • make 能分配数组,map, 但不能分配结构体和原始类型
  • new 能分配数组, map, 结构体和原始类型等的所有类型
  • &Type等效于new
  • 切片不需要分配内存(make,new), 直接声明就可以了…

Golang的反射无法通过一个类型名, 创建其实例

C#有Assembly概念, 可以在一个Assembly里搜索, 创建实例

Golang是静态类型语言, 如果需要, 只能注册你需要创建的结构体, 然后将注册好的map用于创建

Golang可以替换Python来进行复杂的工具流程处理

如果你需要跨平台的工具流程处理, 对Python不熟悉, 可以使用

  1. go run yourcode.go 参数1 参数2

方式来进行工具处理 
觉得慢, 可以编译成可执行文件

这样做的好处: 如果有些类库本身就是go写的, Python想使用是很麻烦的, 而Golang来写则轻而易举

例子: 通过go的protobuf库, 对一些文件进行处理

Golang可以自动持有方法的接收者实例

  1. type myType struct{
  2. }
  3. func (self*myType) foo( p int){
  4. fmt.Println("hello", p )
  5. }
  6. func main(){
  7. var callback func( int )
  8. ins := new(myType)
  9. callback = ins.foo
  10. callback( 100 )
  11. }

做过lua的C++代码绑定的童鞋都清楚: lua只支持外部静态或者全局函数调用 
如果要进行C++类成员函数调用时, 要自己处理this和成员函数 
这种技巧因为早起编译器的虚表不同平台实现细节不统一需要专门处理 
后面跨平台虚表统一后, 类成员函数的调用写法也是很恶心复杂的 
但是Golang的小白式用法, 直接吊打C++, 甚至C#复杂的delegate

LiteIDE篇: 多开秘籍

  • 找到 菜单->查看->选项->通用->存储->存储设置到本地ini文件

  • 关闭LiteIDE

  • 复制LiteIDE整个目录, 命名文件夹为你的工程名

  • 每个工程所在的LiteIDE的配置将是独立的, 不会互相干扰

LiteIDE篇: 测试程序也是可以调试的

别以为程序一定要是main开始的才可以调试

Golang的测试程序虽然都是一个个Test开头的函数,但执行go test时, 还是有main入口

在LiteIDE中, 可以在 调试->开始调试测试程序里进行测试程序调试

LiteIDE篇: 在Windows上可以输出linux可执行文件

go的工具链默认支持交叉编译 
在LiteIDE中, 可以通过切换输出平台, 输出不同平台的可执行文件

posted @ 2016-11-02 11:09 战魂小筑 阅读(3246) | 评论 (1)编辑 收藏

2016年9月3日 #

Google官方为Golang的调试例子默认使用了gdb

然而, 使用gdb调试go程序会遇到goroutine的各类问题, 因为gdb不懂go

因此, 这里使用delve黑科技来进行Golang的程序调试

纯命令行调试方法在网上很容易搜索到, 本文主要以LiteIDE来进行程序调试

关闭编译器优化

正常go build/install出的go程序是完全优化过的, 强行使用调试器挂接调试时, 某些local变量/lamda表达式捕获的变量会直接进入寄存器, 无法使用调试器查看

删掉所有的pkg, 为build或install参数加入关闭编译器优化的参数 -gcflags "-N -l"

例如:

  1. go install -gcflags "-N -l" svc\gamesvc

delve调试器安装方法

LiteIDE自带了gdb, 但是没有delve调试器, 需要自行安装, 命令如下

  1. go get github.com/derekparker/delve/cmd/dlv

delve调试器会被放到你的GOPATH/bin下

LiteIDE中的delve调试器配置

选择调试器

在LiteIDE菜单中选择 调试->debugger/delve

delve环境变量设置

这个时候, LiteIDE依然找不到delve, 因为它不在环境变量PATH中, 这里无需修改环境变量, 只需要LiteIDE的环境配置

在LiteIDE菜单中选择 查看->编辑当前环境, 在弹出的文档中修改

  1. PATH=c:\mingw32\bin;%GOROOT%\bin;%PATH%;c:\your\path\to\delve

去掉PATH前的注释#, 在%PATH%添加分号, 然后和你到delve调试器的路径

开始调试

选择你的工程, 点击F5, 进入调试模式

调试器显示变量值

LiteIDE使用delve调试时, 无法在 变量 监视等窗口中正确捕捉delve调试返回数据(因为格式太复杂了…)

没关系, 我们使用命令行配合显示即可

LiteIDE控制台或调试输出窗口在delve调试时, 实际上是一个标准命令行 
命令如下

  • p 变量名可以查看变量值

  • locals查看局部变量

  • ls可查看当前文件

  • stack查看栈

  • help可以查看各种帮助

调试外部程序

如果你的程序是外部程序, 或者使用go install安装到GOPATH/bin目录的程序, 那么使用delve调试器启动程序时, 可能会碰到启动路径错误的问题

使用LiteIDE菜单 调试->调试其他应用程序… 填入你要调试程序的路径以及工作目录, 可以解决这个问题

posted @ 2016-09-03 18:12 战魂小筑 阅读(3850) | 评论 (0)编辑 收藏

2016年8月12日 #

测试用例

我们对Golang的结构体变量赋值, 以及单参数函数调用进行反射和native操作的测试

 

package main

 

import (

"reflect"

"testing"

)

 

type data struct {

Hp int

}

 

const AssignTimes = 100000000

 

func TestNativeAssign(t *testing.T) {

 

v := data{Hp: 2}

 

for i := 0; i < AssignTimes; i++ {

v.Hp = 3

}

 

}

 

func TestReflectAssign(t *testing.T) {

 

v := data{Hp: 2}

 

vv := reflect.ValueOf(&v).Elem()

 

f := vv.FieldByName("Hp")

 

for i := 0; i < AssignTimes; i++ {

 

f.SetInt(3)

}

 

}

 

func TestReflectFindFieldAndAssign(t *testing.T) {

 

v := data{Hp: 2}

 

vv := reflect.ValueOf(&v).Elem()

 

for i := 0; i < AssignTimes; i++ {

 

vv.FieldByName("Hp").SetInt(3)

}

 

}

 

func foo(v int) {

 

}

 

const CallTimes = 100000000

 

func TestNativeCall(t *testing.T) {

for i := 0; i < CallTimes; i++ {

 

foo(i)

}

}

 

func TestReflectCall(t *testing.T) {

 

v := reflect.ValueOf(foo)

 

for i := 0; i < CallTimes; i++ {

 

v.Call([]reflect.Value{reflect.ValueOf(2)})

}

}

性能测试数据

=== RUN TestNativeAssign
— PASS: TestNativeAssign (0.03s)
=== RUN TestReflectAssign
— PASS: TestReflectAssign (0.41s)
=== RUN TestReflectFindFieldAndAssign
— PASS: TestReflectFindFieldAndAssign (9.86s)
=== RUN TestNativeCall
— PASS: TestNativeCall (0.03s)
=== RUN TestReflectCall
— PASS: TestReflectCall (21.46s)

测试评测

  • 在结构体变量赋值测试用例中, 我们发现TestReflectFindFieldAndAssign赋值格外的耗时. 分析性能点在FieldByName这个函数上, 我们查了下底层如何实现的:

// FieldByName returns the struct field with the given name

// and a boolean to indicate if the field was found.

func (t *structType) FieldByName(name string) (f StructField, present bool) {

// Quick check for top-level name, or struct without anonymous fields.

hasAnon := false

if name != "" {

for i := range t.fields {

tf := &t.fields[i]

if tf.name == nil {

hasAnon = true

continue

}

if *tf.name == name {

return t.Field(i), true

}

}

}

if !hasAnon {

return

}

return t.FieldByNameFunc(func(s string) bool { return s == name })

}

各位看官必须吐槽用for来遍历获取数据, 但冷静下来分析. 这样做无可厚非.
试想如果reflect包在我们使用ValueOf时使用map缓冲好一个结构体所有字段的访问数据后, 肯定访问指定字段速度会很快
但是, 以空间换速度的需求其实最多满足了1%的需求.
同样的例子是图形API里访问Shader变量的方法, 总是默认使用字符串获取, 速度很慢. 当你想快速访问时, 请提前按需缓存字段
那么, Golang使用的也是这样的思路. 虽然暴力了一点, 但是能够让程序跑对, 性能优化的东西放在之后来做, 缓冲下就可以解决

  • 在调用测试用例中, 毫无悬念的, 调用速度很慢
    因此, 我们在平时使用反射时, 尽量偏向于反射变量缓冲存在下的变量赋值或者获取
    而调用的需求尽量减少, 如果有goroutine存在的情况下, 则不必太多担心.
posted @ 2016-08-12 15:26 战魂小筑 阅读(2196) | 评论 (0)编辑 收藏

仅列出标题  下一页