首页 文章 精选 留言 我的

精选列表

搜索[快直播],共10005篇文章
优秀的个人博客,低调大师

StreamX 1.2.3 发布,唯快不破,支持 Flink 1.15

Gitee:https://gitee.com/streamxhub/streamx Github:https://github.com/streamxhub/streamx StreamX让流处理更简单,Flink&Spark极速开发框架,流批一体一站式大数据平台。今天 迎来了 1.2.3 Release 版本的正式发布!本次 增加了诸多新特性,修复了一些 bug ,对 StreamX 的易用性、稳定性等方面进行了加强,欢迎大家下载使用! 1.重要更新 在 1.2.3版本中,StreamX 支持了最新的 Apache Flink 1.15.0,在使用上还是一如既往的简洁丝滑。同时支持了Scala 2.11 / 2.12从此可以自由的选择 Scala、重新划分了 Datastream Connector 模块、使得项目结构更清晰合理,增加了 ES 5 / 6 / 7 的 Datastream Connector ,开放了RESTApi 能力, 使得 StreamX 可以很方便的和其他系统集成。并且修复了一些 bug。具体明细如下: 新增对 Scala 2.12 的支持 新增对Flink 1.15的支持 新增 RestApi与外部系统的集成能力 新增ES5 / 6 / 7Datastreamconnector 新增Flink Cluster 集群管理 ( yarn | k8s ) 新增 Flink SQLPulsarconnector 新增Flink SQLHttpconnector 2.修复增强 修复hadoop 3 环境下kerberos 认证续期相关的 bug 修复项目编译可能存在的不能输出日志的 bug 修复 tm managed memory参数设置不能为0的 bug 修复 jobId 为 0**0导致任务恢复时不能正确识别savepoint 的 bug 修复项目修改后未出现编译按钮, 不能重新编译项目的bug 增强在添加 Flink Home 时对 scala 版本的验证 重构Datastream connector模块,模块和包名重新划分 连接器的示例程序迁移至streamx-quickstart 3.已有能力 系统管理 [√] 用户管理, 新增,修改,删除,多租户支持 [√] 角色管理, 新增,修改,删除 [√] 菜单管理, 新增,修改,删除, 权限管理(到按钮级别的权限控制) [√] REST Token 管理 项目管理 [√] 项目创建,删除(目前只支持git) [√] 项目编译,实时查看编译日志 [√] 项目列表过滤查询,分页 作业管理 [√] 作业创建,删除,编辑,更新,保存,常规参数配置 [√] 作业启动,停止,重启,删除,火焰图(非flink内置) [√] 停止时自动savePoint,启动时从savePoint恢复 [√] 作业列表的查看,过滤,分页。 [√] 可视化 Flink SQL 编辑器,格式化,语法校验,保存 [√] flinkSQL 任务多版本的支持 [√] yarn-per-job 部署模式 [√] yarn-application 部署模式 [√] yarn-session部署模式 [√] k8s-native-application 部署模式 [√] k8s-native-session 部署模式 [√] standalone 部署模式 [√] 任务历史版本备份和回滚 [√] 任务启动失败的日志在线查看 [√] 任务依赖管理,支持标准maven pom坐标的依赖和手动上传jar [√] 任务实时状态显示(实际任务保持一致) [√] 任务监控,失败告警,发送邮件通知,自动重启 [√] 本地上传jar任务的支持 [√] 操作接口开放(REST请求可以控制任务启动|停止|savePoint等) [√] 告警,重启策略, 针对checkpoint连续失败的处理(邮件告警|重启) [√] 支持所有 Flink sql connector [√] 支持所有 Flink UDF 兼容性 [√] Hadoop 2 / 3 的支持 [√] kerberos的认证支持 [√] 多版本flink的支持(flink 1.12.x,1.13.x, 1.14.x, 1.15.0) [√] Scala 2.11 / 2.12 的支持 4.感谢贡献者 StreamX 的发展离不开社区的 Contributor们的付出的积极努力,本次又涌现一批积极的开发者,Flink 1.15、RestApi、Datastream connector重构、Flink Cluster 管理等几个核心特性都是社区的开发者独立贡献的,特别感谢@lvshaokang @lzyyy@wangqingrong @xxyykkxx在上述核心特性中所作的努力,真诚感谢本次所有参与开发测试讨论的小伙伴,以下为 Contributor 名单,排名不分先后 ChunFu、Gilliam、benjobs、chengyuan、huzk、lvshaokang、lzyyy、sober、wangqingrong、wangrui、xxyykkxx、阿洋 5.加入我们 StreamX 遵循 Apache-2.0 开源协议,将会是个长期更新的活跃项目,自项目开源以来就受到很多同行的关注和认可,目前已经登记生产使用的用户有:尚硅谷,INMOBI,JOYME,联通数科...更有百度这样的一线大厂。StreamX 开源刚满一年,目前全网累计2kstar,贡献者共计36位,总代码量已经突破11万行。StreamX于 2021 年 11 月荣获开源中国「最有价值开源项目」。随后荣获「2021 年度 OSC 中国开源项目评选」的「最受欢迎项目」, 目前已经陆续有多家IT教育机构出相关课程, 感谢大家支持, 我们会继续努力, 坚信未来会更好。 [贡献者墙] [StreamX 用户墙] 流批一体,流式数仓,数据湖是大数据领域的趋势,StreamX 虽离这个目标还有一段距离,但我们始终坚信: 道阻且长,行则将至,行而不辍,未来可期。我们会积极进取,做好相关功能持续迭代优化,和社区所有小伙伴一起努力进一步建设好社区,让 StreamX 成为一个功能完善,体验更佳,用户更多的产品,再获得更多认可。真诚欢迎热爱开源的伙伴加入到社区中来,为做一个优秀实用的好项目献上一份自己的力量。 「2021 最受欢迎开源项目」 附: Streamx官网:http://streamxhub.com Github:https://github.com/streamxhub/streamx Gitee:https://gitee.com/streamxhub/streamx

优秀的个人博客,低调大师

快哭了!我被同事写的代码坑惨了

写出整洁的代码,是每个程序员的追求。 《clean code》指出,要想写出好的代码,首先得知道什么是肮脏代码、什么是整洁代码; 然后通过大量的刻意练习,才能真正写出整洁的代码。 图片来自 Pexels WTF/min 是衡量代码质量的唯一标准,Uncle Bob 在书中称糟糕的代码为沼泽(wading),这只突出了我们是糟糕代码的受害者。 国内有一个更适合的词汇:屎山,虽然不是很文雅但是更加客观,程序员既是受害者也是加害者。 对于什么是整洁的代码,书中给出了大师们的总结: Bjarne Stroustrup:优雅且高效;直截了当;减少依赖;只做好一件事 Grady booch:简单直接 Dave thomas:可读,可维护,单元测试 Ron Jeffries:不要重复、单一职责,表达力(Expressiveness) 其中,我最喜欢的是表达力(Expressiveness)这个描述,这个词似乎道出了好代码的真谛:用简单直接的方式描绘出代码的功能,不多也不少。 命名的艺术 坦白的说,命名是一件困难的事情,要想出一个恰到好处的命名需要一番功夫,尤其我们的母语还不是编程语言所通用的英语。 不过这一切都是值得了,好的命名让你的代码更直观,更有表达力。好的命名应该有下面的特征: ①名副其实 好的变量名告诉你:是什么东西,为什么存在,该怎么使用, 如果需要通过注释来解释变量,那么就先得不那么名副其实了。 下面是书中的一个示例代码,展示了命名对代码质量的提升: #badcodedefgetItem(theList):ret=[]forxintheList:ifx[0]==4:ret.append(x)returnret#goodcodedefgetFlaggedCell(gameBoard):'''扫雷游戏,flagged:翻转'''flaggedCells=[]forcellingameBoard:ifcell.IsFlagged():flaggedCells.append(cell)returnflaggedCells ②避免误导 不要挂羊头卖狗肉,不要覆盖惯用缩略语! 这里不得不吐槽前两天才看到的一份代码,居然使用了 l 作为变量名;而且,user 居然是一个 list(单复数都没学好!!) ③有意义的区分 代码是写给机器执行,也是给人阅读的,所以概念一定要有区分度: #baddefcopy(a_list,b_list):pass#gooddefcopy(source,destination):pass ④使用读的出来的单词 如果名称读不出来,那么讨论的时候就会像个傻鸟。 ⑤使用方便搜索的命名 名字长短应与其作用域大小相对应! ⑥避免思维映射 比如在代码中写一个 temp,那么读者就得每次看到这个单词的时候翻译成其真正的意义。 注释 有表达力的代码是无需注释的: The proper use of comments is to compensate for our failure to express ourself in code. 注释的适当作用在于弥补我们用代码表达意图时遇到的失败,这听起来让人沮丧,但事实确实如此。 The truth is in the code,注释只是二手信息,二者的不同步或者不等价是注释的最大问题。 书中给出了一个非常形象的例子来展示,用代码来阐述,而非注释: bad//checktoseeiftheemployeeiseligibleforfullbenefitif((employee.flags&HOURLY_FLAG)&&(employee.age>65))goodif(employee.isEligibleForFullBenefits()) 因此,当想要添加注释的时候,可以想想是否可以通过修改命名,或者修改函数(代码)的抽象层级来展示代码的意图。 当然,也不能因噎废食,书中指出了以下一些情况属于好的注释: 法务信息 对意图的注释,为什么要这么做 警示 TODO 注释 放大看似不合理之物的重要性 其中个人最赞同的是第 2 点和第 5 点,做什么很容易通过命名表达,但为什么要这么做则并不直观,特别涉及到专业知识、算法的时候。 另外,有些第一感觉“不那么优雅”的代码,也许有其特殊愿意,那么这样的代码就应该加上注释,说明为什么要这样,比如为了提升关键路径的性能,可能会牺牲部分代码的可读性。 最坏的注释就是过时或者错误的注释,这对于代码的维护者(也许就是几个月后的自己)是巨大的伤害,可惜除了 code review,并没有简单易行的方法来保证代码与注释的同步。 函数 ①函数的单一职责 一个函数应该只做一件事,这件事应该能通过函数名就能清晰的展示。判断方法很简单:看看函数是否还能再拆出一个函数。 函数要么做什么 do_sth,要么查询什么 query_sth。最恶心的就是函数名表示只会 query_sth,但事实上却会 do_sth,这使得函数产生了副作用。 比如书中的例子: publicclassUserValidator{privateCryptographercryptographer;publicbooleancheckPassword(StringuserName,Stringpassword){Useruser=UserGateway.findByName(userName);if(user!=User.NULL){StringcodedPhrase=user.getPhraseEncodedByPassword();Stringphrase=cryptographer.decrypt(codedPhrase,password);if("ValidPassword".equals(phrase)){Session.initialize();returntrue;}}returnfalse;}} ②函数的抽象层级 每个函数一个抽象层次,函数中的语句都要在同一个抽象层级,不同的抽象层级不能放在一起。 比如我们想把大象放进冰箱,应该是这个样子的: defpushElephantIntoRefrige():openRefrige()pushElephant()closeRefrige() 函数里面的三句代码在同一个层级(高度)描述了要完成把大象放进冰箱这件事顺序相关的三个步骤。 显然,pushElephant 这个步骤又可能包含很多子步骤,但是在 pushElephantIntoRefrige 这个层级,是无需知道太多细节的。 当我们想通过阅读代码的方式来了解一个新的项目时,一般都是采取广度优先的策略,自上而下的阅读代码,先了解整体结构,然后再深入感兴趣的细节。 如果没有对实现细节进行良好的抽象(并凝练出一个名副其实的函数),那么阅读者就容易迷失在细节的汪洋里。 某种程度看来,这个跟金字塔原理也很像: 每一个层级都是为了论证其上一层级的观点,同时也需要下一层级的支持;同一层级之间的多个论点又需要以某种逻辑关系排序。 pushElephantIntoRefrige 就是中心论点,需要多个子步骤的支持,同时这些子步骤之间也有逻辑先后顺序。 ③函数参数 函数的参数越多,组合出的输入情况就愈多,需要的测试用例也就越多,也就越容易出问题。 输出参数相比返回值难以理解,这点深有同感,输出参数实在是很不直观。从函数调用者的角度,一眼就能看出返回值,而很难识别输出参数。输出参数通常逼迫调用者去检查函数签名,这个实在不友好。 向函数传入Boolean(书中称之为 Flag Argument)通常不是好主意。尤其是传入True or False后的行为并不是一件事情的两面,而是两件不同的事情时。 这很明显违背了函数的单一职责约束,解决办法很简单,那就是用两个函数。 Dont repear yourself。 在函数这个层级,是最容易、最直观实现复用的,很多 IDE 也难帮助我们讲一段代码重构出一个函数。 不过在实践中,也会出现这样一种情况: 一段代码在多个方法中都有使用,但是又不完全一样,如果抽象成一个通用函数,那么就需要加参数、加 if else 区别。这样就有点尴尬,貌似可以重构,但又不是很完美。 造成上述问题的某种情况是因为,这段代码也违背了单一职责原则,做了不只一件事情,这才导致不好复用,解决办法是进行方法的细分,才能更好复用。 也可以考虑 template method 来处理差异的部分。 测试 非常惭愧的是,在我经历的项目中,测试(尤其是单元测试)一直都没有得到足够的重视,也没有试行过 TDD。正因为缺失,才更感良好测试的珍贵。 我们常说,好的代码需要有可读性、可维护性、可扩展性,好的代码、架构需要不停的重构、迭代,但自动化测试是保证这一切的基础,没有高覆盖率的、自动化的单元测试、回归测试,谁都不敢去修改代码,只能任其腐烂。 即使针对核心模块写了单元测试,一般也很随意,认为这只是测试代码,配不上生产代码的地位,以为只要能跑通就行了。 这就导致测试代码的可读性、可维护性非常差,然后导致测试代码很难跟随生产代码一起更新、演化,最后导致测试代码失效。所以说,脏测试等同于没测试。 因此,测试代码的三要素: 可读性 可读性 可读性 对于测试的原则、准则如下: 没有测试之前不要写任何功能代码 只编写恰好能够体现一个失败情况的测试代码 只编写恰好能通过测试的功能代码 测试的 FIRST 准则: 快速(Fast)测试应该够快,尽量自动化。 独立(Independent)测试应该应该独立。不要相互依赖 可重复(Repeatable)测试应该在任何环境上都能重复通过。 自我验证(Self-Validating)测试应该有 bool 输出。不要通过查看日志这种低效率方式来判断测试是否通过。 及时(Timely)测试应该及时编写,在其对应的生产代码之前编写。 作者:xybaby 出处:https://www.cnblogs.com/xybaby/p/11335829.html 文末彩蛋 扫码回复," 2020 " , 获得最新前端,后端,大数据,人工智能,PHP等 视频教程资料云盘链接。 编程·思维·职场欢迎扫码关注 本文分享自微信公众号 - 前端迷社区(gh_c8466b051727)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

阿里云AIoT 经典基础知识 快问快答

业务数据流程 我的传感设备,IoT平台,业务服务器,App之间是什么关系? 上行数据链路: 设备以MQTT协议建立和 IoT 物联网平台的长连接,异步PUBLISH数据(Topic和Payload)到 IoT 平台 IoT 平台根据配置的规则引擎,处理数据后,流转到 数据库DB,消息队列MQ,函数计算FC 或者 通过AMQP协议流转到你的ECS服务器上 下行数据链路: ECS服务器程序调用HTTPS的Pub API,发送数据到 IoT 平台 IoT 平台通过MQTT协议,PUBLISH数据到设备端(指定Topic和Payload) FAQ 1.为什么设备无法上报数据? 您需要先定义具有发布权限的通信Topic 2.为什么设备无法接收数据? 您需要先定义具有订阅权限的通信Topic,并且设备主动subscribe此通信Topic定义Topic设备订阅Topic成功 3.设备一定要预先烧录三元组吗? 不需要,参考这个方案 https://developer.aliyun.com/article/770299 4.接入电信NB-IoT设备能对接到阿里云IoT吗? 可以,参考这个方案https://developer.aliyun.com/article/770009! 5.存量设备,不升级改造,能对接到阿里云IoT吗? 可以,参考这个方案https://developer.aliyun.com/article/770009 6.设备上线/离线日志 链路:设备→IoT平台(上线)、设备→IoT平台(离线) 7.物模型-属性上报处理过程的日志 消息链路:设备→IoT平台→物模型校验→物模型数据存储 8.自定义消息规则引擎流转 消息链路:设备→IoT平台→规则引擎→服务端订阅AMQP→业务服务器ECS→服务端订阅AMQP(ACK响应) 9.下行控制指令日志 消息链路:业务服务器ECS(Pub API)→IoT平台(Publish)→设备→IoT平台(PubAck响应) 10.私有协议脚本解析处理日志 消息链路:设备→IoT平台→自定义协议脚本解析→规则引擎→服务端订阅AMQP 【往期回顾】 1.自建MQTT集群迁移阿里云IoT平台2.IoT时代:WiFi配网技术剖析3.微信小程序和IoT智能家居实践4.IoT云端通用数据解析脚本实践

优秀的个人博客,低调大师

《快学 Go 语言》第 14 课 —— 魔术变性指针

本节我们要学习一些 Go 语言的魔法功能,通过内置的 unsafe 包提供的功能,直接操纵指定内存地址的内存。有了 unsafe 包,我们就可以洞悉 Go 语言内置数据结构的内部细节。 unsafe.PointerPointer 代表着变量的内存地址,可以将任意变量的地址转换成 Pointer 类型,也可以将 Pointer 类型转换成任意的指针类型,它是不同指针类型之间互转的中间类型。Pointer 本身也是一个整型的值。 type Pointer int 在 Go 语言里不同类型之间的转换是要受限的。普通的基础变量转换成不同的类型需要进行内存浅拷贝,而指针变量类型之间是禁止直接转换的。要打破这个限制,unsafe.Pointer 就可以派上用场,它允许任意指针类型的互转。 指针的加减运算Pointer 虽然是整型的,但是编译器禁止它直接进行加减运算。如果要进行运算,需要将 Pointer 类型转换 uintptr 类型进行加减,然后再将 uintptr 转换成 Pointer 类型。uintptr 其实也是一个整型。 type uintptr int 下面让我们就来尝试一下刚刚学到的魔法 package main import "fmt" import "unsafe" type Rect struct { Width int Height int } func main() { var r = Rect {50, 50} // *Rect => Pointer => *int => int var width = *(*int)(unsafe.Pointer(&r)) // *Rect => Pointer => uintptr => Pointer => *int => int var height = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + uintptr(8))) fmt.Println(width, height) } ------ 50 50 上面的代码是用 unsafe 包来读取结构体的内容,形式上比较繁琐,注意看代码中的注释,读者需要稍微转一转脑袋来理解一下上面的代码。接下来我们再尝试修改结构体的值 package main import "fmt" import "unsafe" type Rect struct { Width int Height int } func main() { var r = Rect {50, 50} // var pw *int var pw = (*int)(unsafe.Pointer(&r)) // var ph *int var ph = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + uintptr(8))) *pw = 100 *ph = 100 fmt.Println(r.Width, r.Height) } -------- 100 100 代码中的 uintptr(8) 很不优雅,可以使用 unsafe 提供了 Offsetof 方法来替换它,它可以直接得到字段在结构体内的偏移量 var ph = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + unsafe.Offsetof(r.Height)) 你也许会抱怨为啥指针操作这么繁琐,不能简单一点么?Go 语言的设计者故意这样设计的,因为指针操作非常的不安全,所以它要给用户设置障碍。 探索切片内部结构在切片小节,我们知道了切片分为切片头和内部数组两部分,下面我们使用 unsafe 包来验证一下切片的内部数据结构,看看它和我们预期的是否一样。 package main import "fmt" import "unsafe" func main() { // head = {address, 10, 10} // body = [1,2,3,4,5,6,7,8,9,10] var s = []int{1,2,3,4,5,6,7,8,9,10} var address = (**[10]int)(unsafe.Pointer(&s)) var len = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8))) var cap = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16))) fmt.Println(address, *len, *cap) var body = **address for i:=0; i< len(body); i++ { fmt.Printf("%d ", body[i]) } } ------------------ 0xc42000a080 10 10 1 2 3 4 5 6 7 8 9 10 输出的结果正是我们锁期望的,不过读者需要仔细思考一下 address 为什么是二级指针变量。字符串与字节切片的高效转换在字符串小节我们提到字节切片和字符串之间的转换需要复制内存,如果字符串或者字节切片的长度较大,转换起来会有较高的成本。下面我们通过 unsafe 包提供另一种高效的转换方法,让转换前后的字符串和字节切片共享内部存储。字符串和字节切片的不同点在于头部,字符串的头部 2 个 int 字节,切片的头部 3 个 int 字节 package main import "fmt" import "unsafe" func main() { fmt.Println(bytes2str(str2bytes("hello"))) } func str2bytes(s string) []byte { var strhead = *(*[2]int)(unsafe.Pointer(&s)) var slicehead [3]int slicehead[0] = strhead[0] slicehead[1] = strhead[1] slicehead[2] = strhead[1] return *(*[]byte)(unsafe.Pointer(&slicehead)) } func bytes2str(bs []byte) string { return *(*string)(unsafe.Pointer(&bs)) } ----- hello 切记通过这种形式转换而成的字节切片千万不可以修改,因为它的底层字节数组是共享的,修改会破坏字符串的只读规则。其次使用这种形式得到的字符串或者切片只可以用作临时的局部变量,因为被共享的字节数组随时可能会被回收,原字符串或者字节切片的内存由于不再被引用,让垃圾回收器解决掉了。 深入接口变量的赋值在接口变量的小节,有一个问题还悬而未决,那就是接口变量在赋值时发生了什么? 通过 unsafe 包,我们就可以看清里面的细节,下面我们将一个结构体变量赋值给接口变量,看看修改结构体的内存会不会影响到接口变量的数据内存 package main import "fmt" import "unsafe" type Rect struct { Width int Height int } func main() { var r = Rect{50, 50} // {typeptr, dataptr} var s interface{} = r var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s)) // var dataptr *Rect var sdataptr = sptrs[1] fmt.Println(sdataptr.Width, sdataptr.Height) // 修改原对象,看看接口指向的对象是否受到影响 r.Width = 100 fmt.Println(sdataptr.Width, sdataptr.Height) } ------- 50 50 50 50 从输出中可以得出结论,将结构体变量赋值给接口变量,结构体内存会被复制。那如果是两个接口变量之间的赋值呢,会不会同样也需要复制指向的数据呢? package main import "fmt" import "unsafe" type Rect struct { Width int Height int } func main() { // {typeptr, dataptr} var s interface{} = Rect{50, 50} var r = s var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r)) var rdataptr = rptrs[1] var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s)) var sdataptr = sptrs[1] fmt.Println(sdataptr.Width, sdataptr.Height) fmt.Println(rdataptr.Width, rdataptr.Height) // 修改原对象 sdataptr.Width = 100 // 再对比一下原对象和目标对象 fmt.Println(sdataptr.Width, sdataptr.Height) fmt.Println(rdataptr.Width, rdataptr.Height) } ----------- 50 50 50 50 100 50 100 50 从输出中可以发现赋值前后两个接口变量共享了数据内存,没有发生数据的复制。接下来我们再引入第 3 个问题,不同类型的接口变量赋值会不会发生复制? package main import "fmt" import "unsafe" type Areable interface { Area() int } type Rect struct { Width int Height int } func (r Rect) Area() int { return r.Width * r.Height } func main() { // {typeptr, dataptr} var s Areable = Rect{50, 50} var r interface{} = s var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r)) var rdataptr = rptrs[1] var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s)) var sdataptr = sptrs[1] fmt.Println(sdataptr.Width, sdataptr.Height) fmt.Println(rdataptr.Width, rdataptr.Height) // 修改原对象 sdataptr.Width = 100 // 再对比一下原对象和目标对象 fmt.Println(sdataptr.Width, sdataptr.Height) fmt.Println(rdataptr.Width, rdataptr.Height) } ------ 50 50 50 50 100 50 100 50 结果是不同类型接口之间赋值指向的数据对象还是共享的。接下来我们再引入第 4 个 问题,接口类型之间在造型时是否会发生内存的复制。 package main import "fmt" import "unsafe" type Areable interface { Area() int } type Rect struct { Width int Height int } func (r Rect) Area() int { return r.Width * r.Height } func main() { // {typeptr, dataptr} var s interface{} = Rect{50, 50} var r Areable = s.(Areable) var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r)) var rdataptr = rptrs[1] var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s)) var sdataptr = sptrs[1] fmt.Println(sdataptr.Width, sdataptr.Height) fmt.Println(rdataptr.Width, rdataptr.Height) // 修改原对象 sdataptr.Width = 100 // 再对比一下原对象和目标对象 fmt.Println(sdataptr.Width, sdataptr.Height) fmt.Println(rdataptr.Width, rdataptr.Height) } ------ 50 50 50 50 100 50 100 50 答案是不同接口类型之间造型数据还是共享的。最后再提一个问题,将接口类型造型成结构体类型,是否会发生内存复制? package main import "fmt" import "unsafe" type Areable interface { Area() int } type Rect struct { Width int Height int } func (r Rect) Area() int { return r.Width * r.Height } func main() { // {typeptr, dataptr} var s interface{} = Rect{50, 50} var r Rect = s.(Rect) var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s)) var sdataptr = sptrs[1] // 修改原对象 sdataptr.Width = 100 // 再对比一下原对象和目标对象 fmt.Println(sdataptr.Width, sdataptr.Height) fmt.Println(r.Width, r.Height) } 答案是将接口造型成结构体类型,内存会发生复制,它们之间的数据不会共享。 从上面 5 个 问题,我们可以得出结论,接口类型和结构体类型似乎是两个不同的世界。只有接口类型之间的赋值和转换会共享数据,其它情况都会复制数据,其它情况包括结构体之间的赋值,结构体转接口,接口转结构体。不同接口变量之间的转换本质上只是调整了接口变量内部的类型指针,数据指针并不会发生改变。 通过 unsafe 包我们还可以分析很多细节,在高级内容部分,我们将会频繁使用这个工具。 原文发布时间为:2018-12-19本文作者:老钱本文来自云栖社区合作伙伴“码洞”,了解相关信息可以关注“codehole”微信公众号

优秀的个人博客,低调大师

《快学 Go 语言》第 13 课 —— 并发与安全

上一节我们提到并发编程不同的协程共享数据的方式除了通道之外还有就是共享变量。虽然 Go 语言官方推荐使用通道的方式来共享数据,但是通过变量来共享才是基础,因为通道在底层也是通过共享变量的方式来实现的。通道的内部数据结构包含一个数组,对通道的读写就是对内部数组的读写。 在并发环境下共享读写变量必须要使用锁来控制数据结构的安全,Go 语言内置了 sync 包,里面包含了我们平时需要经常使用的互斥锁对象 sync.Mutex。Go 语言内置的字典不是线程安全的,所以下面我们尝试使用互斥锁对象来保护字典,让它变成线程安全的字典。 线程不安全的字典Go 语言内置了数据结构「竞态检查」工具来帮我们检查程序中是否存在线程不安全的代码。当我们在运行代码时,打开 -run 开关,程序就会在内置的通用数据结构中进行埋点检查。竞态检查工具在 Go 1.1 版本中引入,该功能帮助 Go 语言「元团队」找出了 Go 语言标准库中几十个存在线程安全隐患的 bug,这是一个非常了不起的功能。同时这也说明了即使是猿界的神仙,写出来的代码也避免不了有 bug。下面我们来尝试一下 package main import "fmt" func write(d map[string]int) { d["fruit"] = 2 } func read(d map[string]int) { fmt.Println(d["fruit"]) } func main() { d := map[string]int{} go read(d) write(d) } 上面的代码明显存在安全隐患,运行下面的竞态检查指令观察输出结果 $ go run -race main.go ================== WARNING: DATA RACE Read at 0x00c420090180 by goroutine 6: runtime.mapaccess1_faststr() /usr/local/Cellar/go/1.10.3/libexec/src/runtime/hashmap_fast.go:172 +0x0 main.read() ~/go/src/github.com/pyloque/practice/main.go:10 +0x5d Previous write at 0x00c420090180 by main goroutine: runtime.mapassign_faststr() /usr/local/Cellar/go/1.10.3/libexec/src/runtime/hashmap_fast.go:694 +0x0 main.main() ~/go/src/github.com/pyloque/practice/main.go:6 +0x88 Goroutine 6 (running) created at: main.main() ~/go/src/github.com/pyloque/practice/main.go:15 +0x59 ================== ================== WARNING: DATA RACE Read at 0x00c4200927d8 by goroutine 6: main.read() ~/go/src/github.com/pyloque/practice/main.go:10 +0x70 Previous write at 0x00c4200927d8 by main goroutine: main.main() ~/go/src/github.com/pyloque/practice/main.go:6 +0x9b Goroutine 6 (running) created at: main.main() ~/go/src/github.com/pyloque/practice/main.go:15 +0x59 ================== 2 Found 2 data race(s) 竞态检查工具是基于运行时代码检查,而不是通过代码静态分析来完成的。这意味着那些没有机会运行到的代码逻辑中如果存在安全隐患,它是检查不出来的。 线程安全的字典让字典变的线程安全,就需要对字典的所有读写操作都使用互斥锁保护起来。 package main import "fmt" import "sync" type SafeDict struct { data map[string]int mutex *sync.Mutex } func NewSafeDict(data map[string]int) *SafeDict { return &SafeDict{ data: data, mutex: &sync.Mutex{}, } } func (d *SafeDict) Len() int { d.mutex.Lock() defer d.mutex.Unlock() return len(d.data) } func (d *SafeDict) Put(key string, value int) (int, bool) { d.mutex.Lock() defer d.mutex.Unlock() old_value, ok := d.data[key] d.data[key] = value return old_value, ok } func (d *SafeDict) Get(key string) (int, bool) { d.mutex.Lock() defer d.mutex.Unlock() old_value, ok := d.data[key] return old_value, ok } func (d *SafeDict) Delete(key string) (int, bool) { d.mutex.Lock() defer d.mutex.Unlock() old_value, ok := d.data[key] if ok { delete(d.data, key) } return old_value, ok } func write(d *SafeDict) { d.Put("banana", 5) } func read(d *SafeDict) { fmt.Println(d.Get("banana")) } func main() { d := NewSafeDict(map[string]int{ "apple": 2, "pear": 3, }) go read(d) write(d) } 尝试使用竞态检查工具运行上面的代码,会发现没有了刚才一连串的警告输出,说明 Get 和 Put 方法已经做到了协程安全,但是还不能说明 Delete() 方法是否安全,因为它根本没有机会得到运行。 在上面的代码中我们再次看到了 defer 语句的应用场景 —— 释放锁。defer 语句总是要推迟到函数尾部运行,所以如果函数逻辑运行时间比较长,这会导致锁持有的时间较长,这时使用 defer 语句来释放锁未必是一个好注意。 避免锁复制上面的代码中还有一个需要特别注意的地方是 sync.Mutex 是一个结构体对象,这个对象在使用的过程中要避免被复制 —— 浅拷贝。复制将会导致锁被「分裂」了,也就起不到保护的作用。所以在平时的使用中要尽量使用它的指针类型。读者可以尝试将上面的类型换成非指针类型,然后运行一下竞态检查工具,会看到警告信息再次布满整个屏幕。锁复制存在于结构体变量的赋值、函数参数传递、方法参数传递中,都需要注意。 使用匿名锁字段在结构体章节,我们知道外部结构体可以自动继承匿名内部结构体的所有方法。如果将上面的 SafeDict 结构体进行改造,将锁字段匿名,就可以稍微简化一下代码。 package main import "fmt" import "sync" type SafeDict struct { data map[string]int *sync.Mutex } func NewSafeDict(data map[string]int) *SafeDict { return &SafeDict{data, &sync.Mutex{}} } func (d *SafeDict) Len() int { d.Lock() defer d.Unlock() return len(d.data) } func (d *SafeDict) Put(key string, value int) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] d.data[key] = value return old_value, ok } func (d *SafeDict) Get(key string) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] return old_value, ok } func (d *SafeDict) Delete(key string) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] if ok { delete(d.data, key) } return old_value, ok } func write(d *SafeDict) { d.Put("banana", 5) } func read(d *SafeDict) { fmt.Println(d.Get("banana")) } func main() { d := NewSafeDict(map[string]int{ "apple": 2, "pear": 3, }) go read(d) write(d) } 使用读写锁日常应用中,大多数并发数据结构都是读多写少的,对于读多写少的场合,可以将互斥锁换成读写锁,可以有效提升性能。sync 包也提供了读写锁对象 RWMutex,不同于互斥锁只有两个常用方法 Lock() 和 Unlock(),读写锁提供了四个常用方法,分别是写加锁 Lock()、写释放锁 Unlock()、读加锁 RLock() 和读释放锁 RUnlock()。写锁是拍他锁,加写锁时会阻塞其它协程再加读锁和写锁,读锁是共享锁,加读锁还可以允许其它协程再加读锁,但是会阻塞加写锁。 读写锁在写并发高的情况下性能退化为普通的互斥锁 下面我们将上面代码中 SafeDict 的互斥锁改造成读写锁。 package main import "fmt" import "sync" type SafeDict struct { data map[string]int *sync.RWMutex } func NewSafeDict(data map[string]int) *SafeDict { return &SafeDict{data, &sync.RWMutex{}} } func (d *SafeDict) Len() int { d.RLock() defer d.RUnlock() return len(d.data) } func (d *SafeDict) Put(key string, value int) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] d.data[key] = value return old_value, ok } func (d *SafeDict) Get(key string) (int, bool) { d.RLock() defer d.RUnlock() old_value, ok := d.data[key] return old_value, ok } func (d *SafeDict) Delete(key string) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] if ok { delete(d.data, key) } return old_value, ok } func write(d *SafeDict) { d.Put("banana", 5) } func read(d *SafeDict) { fmt.Println(d.Get("banana")) } func main() { d := NewSafeDict(map[string]int{ "apple": 2, "pear": 3, }) go read(d) write(d) } 原文发布时间为:2018-12-14本文作者:老钱本文来自云栖社区合作伙伴“ 码洞”,了解相关信息可以关注“codehole”微信公众号

优秀的个人博客,低调大师

《快学 Go 语言》第 10 课 —— 错误与异常

import "os"import "fmt" ..... 在这段代码里有几个点需要特别注意。第一个需要注意的是 os.Open()、f.Read() 函数返回了两个值,Go 语言不但允许函数返回两个值,三个值四个值都是可以的,只不过 Go 语言普遍没有使用多返回值的习惯,仅仅是在需要返回错误的时候才会需要两个返回值。除了错误之外,还有一个地方需要两个返回值,那就是字典,通过第二个返回值来告知读取的结果是零值还是根本就不存在。 var score, ok := scores["apple"] 第二个需要注意的是 defer 关键字,它将文件的关闭调用推迟到当前函数的尾部执行,即使后面的代码抛出了异常,文件关闭也会确保被执行,相当于 Java 语言的 finally 语句块。defer 是 Go 语言非常重要的特性,在日常应用开发中,我们会经常使用到它。 第三个需要注意的地方是 append 函数参数中出现了 … 符号。在切片章节,我们知道 append 函数可以将单个元素追加到切片中,其实 append 函数可以一次性追加多个元素,它的参数数量是可变的。 var s = []int{1,2,3,4,5} s = append(s,6,7,8,9) 但是读文件的代码中需要将整个切片的内容追加到另一个切片中,这时候就需要 … 操作符,它的作用是将切片参数的所有元素展开后传递给 append 函数。你可能会担心如果切片里有成百上千的元素,展开成元素再传递会不会非常耗费性能。这个不必担心,展开只是形式上的展开,在实现上其实并没有展开,传递过去的参数本质上还是切片。 第四个需要注意的地方是读文件操作 f.Read() ,它会将文件的内容往切片里填充,填充的量不会超过切片的长度(注意不是容量)。如果将缓冲改成下面这种形式,就会死循环! var buf = make([]byte, 0, 100) 另外如果遇到文件尾了,切片就不会填满。所以需要通过返回值 n 来明确到底读了多少字节。 体验 Redis 的错误处理 上面读文件的例子并没有让读者感受到错误处理的不爽,下面我们要引入 Go 语言 Redis 的客户端包,来真实体验一下 Go 语言的错误处理有多让人不快。 使用第三方包,需要使用 go get 指令下载这个包,该指令会将第三方包放到 GOPATH 目录下。 go get github.com/go-redis/redis 下面我要实现一个小功能,获取 Redis 中两个整数值,然后相乘,再存入 Redis 中 package mainimport "fmt"import "strconv"import "github.com/go-redis/redis"func main() {// 定义客户端对象,内部包含一个连接池var client = redis.NewClient(&redis.Options { Addr: "localhost:6379", })// 定义三个重要的整数变量值,默认都是零var val1, val2, val3 int// 获取第一个值 valstr1, err := client.Get("value1").Result()if err == nil { val1, err = strconv.Atoi(valstr1)if err != nil { fmt.Println("value1 not a valid integer")return } } else if err != redis.Nil { fmt.Println("redis access error reason:" + err.Error())return }// 获取第二个值 valstr2, err := client.Get("value2").Result()if err == nil { val2, err = strconv.Atoi(valstr2)if err != nil { fmt.Println("value1 not a valid integer")return } } else if err != redis.Nil { fmt.Println("redis access error reason:" + err.Error())return }// 保存第三个值 val3 = val1 * val2 ok, err := client.Set("value3",val3, 0).Result()if err != nil { fmt.Println("set value error reason:" + err.Error())return } fmt.Println(ok) } ------ OK 因为 Go 语言中不轻易使用异常语句,所以对于任何可能出错的地方都需要判断返回值的错误信息。上面代码中除了访问 Redis 需要判断之外,字符串转整数也需要判断。 另外还有一个需要特别注意的是因为字符串的零值是空串而不是 nil,你不好从字符串内容本身判断出 Redis 是否存在这个 key 还是对应 key 的 value 为空串,需要通过返回值的错误信息来判断。代码中的 redis.Nil 就是客户端专门为 key 不存在这种情况而定义的错误对象。 相比于写习惯了 Python 和 Java 程序的朋友们来说,这样繁琐的错误判断简直太地狱了。不过还是那句话,习惯了就好。 异常与捕捉 Go 语言提供了 panic 和 recover 全局函数让我们可以抛出异常、捕获异常。它类似于其它高级语言里常见的 throw try catch 语句,但是又很不一样,比如 panic 函数可以抛出来任意对象。下面我们看一个使用 panic 的例子 package mainimport "fmt"var negErr = fmt.Errorf("non positive number")func main() { fmt.Println(fact(10)) fmt.Println(fact(5)) fmt.Println(fact(-5)) fmt.Println(fact(15)) }// 让阶乘函数返回错误太不雅观了// 使用 panic 会合适一些func fact(a int) int{if a <= 0 {panic(negErr) }var r = 1for i :=1;i<=a;i++ { r *= i }return r } -------3628800120panic: non positive number goroutine 1 [running]: main.fact(0xfffffffffffffffb, 0x1) /Users/qianwp/go/src/github.com/pyloque/practice/main.go:16 +0x75 main.main() /Users/qianwp/go/src/github.com/pyloque/practice/main.go:10 +0x122 exit status 2 上面的代码抛出了 negErr,直接导致了程序崩溃,程序最后打印了异常堆栈信息。下面我们使用 recover 函数来保护它,recover 函数需要结合 defer 语句一起使用,这样可以确保 recover() 逻辑在程序异常的时候也可以得到调用。 package mainimport "fmt"var negErr = fmt.Errorf("non positive number")func main() {defer func() {if err := recover(); err != nil { fmt.Println("error catched", err) } }() fmt.Println(fact(10)) fmt.Println(fact(5)) fmt.Println(fact(-5)) fmt.Println(fact(15)) }func fact(a int) int{if a <= 0 {panic(negErr) }var r = 1for i :=1;i<=a;i++ { r *= i }return r } -------3628800120 error catched non positive number 输出结果中的异常堆栈信息没有了,说明捕获成功了,不过即使程序不再崩溃,异常点后面的逻辑也不会再继续执行了。上面的代码中需要注意的是我们使用了匿名函数 func() {…} defer func() {if err := recover(); err != nil { fmt.Println("error catched", err) } }() 尾部还有个括号是怎么回事,为什么还需要这个括号呢?它表示对匿名函数进行了调用。对比一下前面写的文件关闭尾部的括号就能理解了 defer f.Close() 还有个值得注意的地方时,panic 抛出的对象未必是错误对象,而 recover() 返回的对象正是 panic 抛出来的对象,所以它也不一定是错误对象。 func panic(v interface{})func recover() interface{} 我们经常还需要对 recover() 返回的结果进行判断,以挑选出我们愿意处理的异常对象类型,对于那些不愿意处理的,可以选择再次抛出来,让上层来处理。 defer func() {if err := recover(); err != nil {if err == negErr { fmt.Println("error catched", err) } else {panic(err) // rethrow } } }() 异常的真实应用 Go 语言官方表态不要轻易使用 panic recover,除非你真的无法预料中间可能会发生的错误,或者它能非常显著地简化你的代码。简单一点说除非逼不得已,否则不要使用它。 在一个常见的 Web 应用中,不能因为个别 URL 处理器抛出异常而导致整个程序崩溃,就需要在每个 URL 处理器外面包括一层 recover() 来恢复异常。 图片 在 json 序列化过程中,逻辑上需要递归处理 json 内部的各种类型,每一种容器类型内部都可能会遇到不能序列化的类型。如果对每个函数都使用返回错误的方式来编写代码,会显得非常繁琐。所以在内置的 json 包里也使用了 panic,然后在调用的最外层包裹了 recover 函数来进行恢复,最终统一返回一个 error 类型。 图片 你可以想象一下,内置 json 包的开发者在设计开发这个包的时候应该也是纠结的焦头烂额,最终还是使用了 panic 和 recover 来让自己的代码变的好看一些。 多个 defer 语句 有时候我们需要在一个函数里使用多次 defer 语句。比如拷贝文件,需要同时打开源文件和目标文件,那就需要调用两次 defer f.Close()。 package mainimport "fmt"import "os"func main() { fsrc, err := os.Open("source.txt")if err != nil { fmt.Println("open source file failed")return }defer fsrc.Close() fdes, err := os.Open("target.txt")if err != nil { fmt.Println("open target file failed")return }defer fdes.Close() fmt.Println("do something here") } 需要注意的是 defer 语句的执行顺序和代码编写的顺序是反过来的,也就是说最先 defer 的语句最后执行,为了验证这个规则,我们来改写一下上面的代码 package mainimport "fmt"import "os"func main() { fsrc, err := os.Open("source.txt")if err != nil { fmt.Println("open source file failed")return }defer func() { fmt.Println("close source file") fsrc.Close() }() fdes, err := os.Open("target.txt")if err != nil { fmt.Println("open target file failed")return }defer func() { fmt.Println("close target file") fdes.Close() }() fmt.Println("do something here") } -------- do something hereclose target fileclose source file 下一节我们开讲 Go 语言最重要的特色功能 —— 通道与协程 原文发布时间为: 2018-11-29 本文作者: 老钱 本文来自云栖社区合作伙伴“ 码洞”,了解相关信息可以关注“ 码洞”。

优秀的个人博客,低调大师

《快学 Go 语言》第 5 课 —— 神奇的切片

切片无疑是 Go 语言中最重要的数据结构,也是最有趣的数据结构,它的英文词汇叫 slice。所有的 Go 语言开发者都津津乐道地谈论切片的内部机制,它也是 Go 语言技能面试中面试官最爱问的知识点之一。初级用户很容易滥用它,这小小的切片想要彻底的理解它是需要花费一番功夫的。在使用切片之前,我觉得很有必要将切片的内部结构做一下说明。 学过 Java 语言的人会比较容易理解切片,因为它的内部结构非常类似于 ArrayList,ArrayList 的内部实现也是一个数组。当数组容量不够需要扩容时,就会换新的数组,还需要将老数组的内容拷贝到新数组。ArrayList 内部有两个非常重要的属性 capacity 和 length。capacity 表示内部数组的总长度,length 表示当前已经使用的数组的长度。length 永远不能超过 capacity。 图片 上图中一个切片变量包含三个域,分别是底层数组的指针、切片的长度 length 和切片的容量 capacity。切片支持 append 操作可以将新的内容追加到底层数组,也就是填充上面的灰色格子。如果格子满了,切片就需要扩容,底层的数组就会更换。 形象一点说,切片变量是底层数组的视图,底层数组是卧室,切片变量是卧室的窗户。通过窗户我们可以看见底层数组的一部分或全部。一个卧室可以有多个窗户,不同的窗户能看到卧室的不同部分。 切片的创建 切片的创建有多种方式,我们先看切片最通用的创建方法,那就是内置的 make 函数 package mainimport "fmt"func main() {var s1 []int = make([]int, 5, 8)var s2 []int = make([]int, 8) // 满容切片 fmt.Println(s1) fmt.Println(s2) } ------------- [0 0 0 0 0] [0 0 0 0 0 0 0 0] make 函数创建切片,需要提供三个参数,分别是切片的类型、切片的长度和容量。其中第三个参数是可选的,如果不提供第三个参数,那么长度和容量相等,也就是说切片的满容的。切片和普通变量一样,也可以使用类型自动推导,省去类型定义以及 var 关键字。比如上面的代码和下面的代码是等价的。 package mainimport "fmt"func main() {var s1 = make([]int, 5, 8) s2 := make([]int, 8) fmt.Println(s1) fmt.Println(s2) } ------------- [0 0 0 0 0] [0 0 0 0 0 0 0 0] 切片的初始化 使用 make 函数创建的切片内容是「零值切片」,也就是内部数组的元素都是零值。Go 语言还提供了另一个种创建切片的语法,允许我们给它赋初值。使用这种方式创建的切片是满容的。 package mainimport "fmt"func main() {var s []int = []int{1,2,3,4,5} // 满容的 fmt.Println(s, len(s), cap(s)) } --------- [1 2 3 4 5] 5 5 Go 语言提供了内置函数 len() 和 cap() 可以直接获得切片的长度和容量属性。 空切片 在创建切片时,还有两个非常特殊的情况需要考虑,那就是容量和长度都是零的切片,叫着「空切片」,这个不同于前面说的「零值切片」。 package mainimport "fmt"func main() {var s1 []intvar s2 []int = []int{}var s2 []int = make([]int, 0) fmt.Println(s1, s2, s3) fmt.Println(len(s1), len(s2), len(s3)) fmt.Println(cap(s1), cap(s2), cap(s3)) } ----------- [] [] []0 0 00 0 0 上面三种形式创建的切片都是「空切片」,不过在内部结构上这三种形式是有差异的,甚至第一种都不叫「空切片」,而是叫着「 nil 切片」。但是在形式上它们几乎一摸一样,用起来差不多没有区别。所以初级用户可以不必区分「空切片」和「 nil 切片」,到后续章节我们会仔细分析这两种形式的区别。 切片的赋值 切片的赋值是一次浅拷贝操作,拷贝的是切片变量的三个域,你可以将切片变量看成长度为 3 的 int 型数组,数组的赋值就是浅拷贝。拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。 package mainimport "fmt"func main() {var s1 = make([]int, 5, 8)// 切片的访问和数组差不多for i := 0; i < len(s1); i++ { s1[i] = i + 1 }var s2 = s1 fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2))// 尝试修改切片内容 s2[0] = 255 fmt.Println(s1) fmt.Println(s2) } -------- [1 2 3 4 5] 5 8 [1 2 3 4 5] 5 8 [255 2 3 4 5] [255 2 3 4 5] 从上面的输出中可以看到赋值的两切片共享了底层数组。 切片的遍历 切片在遍历的语法上和数组是一样的,除了支持下标遍历外,那就是使用 range 关键字 package mainimport "fmt"func main() {var s = []int{1,2,3,4,5}for index := range s { fmt.Println(index, s[index]) }for index, value := range s { fmt.Println(index, value) } } --------0 11 22 33 44 50 11 22 33 44 5 文章开头提到切片是动态的数组,其长度是可以变化的。什么操作可以改变切片的长度呢,这个操作就是追加操作。切片每一次追加后都会形成新的切片变量,如果底层数组没有扩容,那么追加前后的两个切片变量共享底层数组,如果底层数组扩容了,那么追加前后的底层数组是分离的不共享的。如果底层数组是共享的,一个切片的内容变化就会影响到另一个切片,这点需要特别注意。 package mainimport "fmt"func main() {var s1 = []int{1,2,3,4,5} fmt.Println(s1, len(s1), cap(s1))// 对满容的切片进行追加会分离底层数组var s2 = append(s1, 6) fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2))// 对非满容的切片进行追加会共享底层数组var s3 = append(s2, 7) fmt.Println(s2, len(s2), cap(s2)) fmt.Println(s3, len(s3), cap(s3)) } -------------------------- [1 2 3 4 5] 5 5 [1 2 3 4 5] 5 5 [1 2 3 4 5 6] 6 10 [1 2 3 4 5 6] 6 10 [1 2 3 4 5 6 7] 7 10 正是因为切片追加后是新的切片变量,Go 编译器禁止追加了切片后不使用这个新的切片变量,以避免用户以为追加操作的返回值和原切片变量是同一个变量。 package mainimport "fmt"func main() {var s1 = []int{1,2,3,4,5}append(s1, 6) fmt.Println(s1) } -------------- ./main.go:7:8: append(s1, 6) evaluated but not used 如果你真的不需要使用这个新的变量,可以将 append 的结果赋值给下划线变量。下划线变量是 Go 语言特殊的内置变量,它就像一个黑洞,可以将任意变量赋值给它,但是却不能读取这个特殊变量。 package mainimport "fmt"func main() {var s1 = []int{1,2,3,4,5} _ = append(s1, 6) fmt.Println(s1) } ---------- [1 2 3 4 5] 还需要注意的是追加虽然会导致底层数组发生扩容,更换的新的数组,但是旧数组并不会立即被销毁被回收,因为老切片还指向这旧数组。 切片的域是只读的 我们刚才说切片的长度是可以变化的,为什么又说切片是只读的呢?这不是矛盾么。这是为了提醒读者注意切片追加后形成了一个新的切片变量,而老的切片变量的三个域其实并不会改变,改变的只是底层的数组。这里说的是切片的「域」是只读的,而不是说切片是只读的。切片的「域」就是组成切片变量的三个部分,分别是底层数组的指针、切片的长度和切片的容量。这里读者需要仔细咀嚼。 切割切割 到目前位置还没有说明切片名字的由来,既然叫着切片,那总得可以切割吧。切割切割,有些男娃子听到这个词汇时身上会起鸡皮疙瘩。切片的切割可以类比字符串的子串,它并不是要把切片割断,而是从母切片中拷贝出一个子切片来,子切片和母切片共享底层数组。下面我们来看一下切片究竟是如何切割的。 package mainimport "fmt"func main() {var s1 = []int{1,2,3,4,5,6,7}// start_index 和 end_index,不包含 end_index// [start_index, end_index)var s2 = s1[2:5] fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2)) } ------------ [1 2 3 4 5 6 7] 7 7 [3 4 5] 3 5 上面的输出需要特别注意的是,既然切割前后共享底层数组,那为什么容量不一样呢?解释它我必须要画图了,读者请务必仔细观察下面这张图 图片 我们注意到子切片的内部数据指针指向了数组的中间位置,而不再是数组的开头了。子切片容量的大小是从中间的位置开始直到切片末尾的长度,母子切片依旧共享底层数组。 子切片语法上要提供起始和结束位置,这两个位置都可选的,不提供起始位置,默认就是从母切片的初始位置开始(不是底层数组的初始位置),不提供结束位置,默认就结束到母切片尾部(是长度线,不是容量线)。下面我们看个例子 package mainimport "fmt"func main() {var s1 = []int{1, 2, 3, 4, 5, 6, 7}var s2 = s1[:5]var s3 = s1[3:]var s4 = s1[:] fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2)) fmt.Println(s3, len(s3), cap(s3)) fmt.Println(s4, len(s4), cap(s4)) } ----------- [1 2 3 4 5 6 7] 7 7 [1 2 3 4 5] 5 7 [4 5 6 7] 4 4 [1 2 3 4 5 6 7] 7 7 细心的同学可能会注意到上面的 s1[:] 很特别,它和普通的切片赋值有区别么?答案是没区别,这非常让人感到意外,同样的共享底层数组,同样是浅拷贝。下面我们来验证一下 package mainimport "fmt"func main() {var s = make([]int, 5, 8)for i:=0;i<len(s);i++ { s[i] = i+1 } fmt.Println(s, len(s), cap(s))var s2 = svar s3 = s[:] fmt.Println(s2, len(s2), cap(s2)) fmt.Println(s3, len(s3), cap(s3))// 修改母切片 s[0] = 255 fmt.Println(s, len(s), cap(s)) fmt.Println(s2, len(s2), cap(s2)) fmt.Println(s3, len(s3), cap(s3)) } ------------- [1 2 3 4 5] 5 8 [1 2 3 4 5] 5 8 [1 2 3 4 5] 5 8 [255 2 3 4 5] 5 8 [255 2 3 4 5] 5 8 [255 2 3 4 5] 5 8 使用过 Python 的同学可能会问,切片支持负数的位置么,答案是不支持,下标不可以是负数。 数组变切片 对数组进行切割可以转换成切片,切片将原数组作为内部底层数组。也就是说修改了原数组会影响到新切片,对切片的修改也会影响到原数组。 package mainimport "fmt"func main() {var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}var b = a[2:6] fmt.Println(b) a[4] = 100 fmt.Println(b) } ------- [3 4 5 6] [3 4 100 6] Go 语言还内置了一个 copy 函数,用来进行切片的深拷贝。不过其实也没那么深,只是深到底层的数组而已。如果数组里面装的是指针,比如 []*int 类型,那么指针指向的内容还是共享的。 func copy(dst, src []T) int copy 函数不会因为原切片和目标切片的长度问题而额外分配底层数组的内存,它只负责拷贝数组的内容,从原切片拷贝到目标切片,拷贝的量是原切片和目标切片长度的较小值 —— min(len(src), len(dst)),函数返回的是拷贝的实际长度。我们来看一个例子 package mainimport "fmt"func main() {var s = make([]int, 5, 8)for i:=0;i<len(s);i++ { s[i] = i+1 } fmt.Println(s)var d = make([]int, 2, 6)var n = copy(d, s) fmt.Println(n, d) } ----------- [1 2 3 4 5]2 [1 2] 当比较短的切片扩容时,系统会多分配 100% 的空间,也就是说分配的数组容量是切片长度的2倍。但切片长度超过1024时,扩容策略调整为多分配 25% 的空间,这是为了避免空间的过多浪费。试试解释下面的运行结果。 s1 := make([]int, 6) s2 := make([]int, 1024) s1 = append(s1, 1) s2 = append(s2, 2) fmt.Println(len(s1), cap(s1)) fmt.Println(len(s2), cap(s2)) -------------------------------------------7 121025 1344 扩容是一个比较复杂的操作,内部的细节必须通过分析源码才能知晓,不去理解扩容的细节并不会影响到平时的使用,所以关于切片的源码我们后续在高级内容里面再仔细分析。 原文发布时间为: 2018-11-12 本文作者:码洞 本文来自云栖社区合作伙伴“码洞”,了解相关信息可以关注“码洞”。

优秀的个人博客,低调大师

《快学 Go 语言》第 4 课 —— 低调的数组

只要掌握了数据结构中的四大法宝,就可以包打天下,他们是:array 、linked list 、hash table、binary tree 。这四大法宝可不是各自为战的,灵活结合才能游刃有余。比如,一个用 hash table 组织的 symbol table,其中个个都是由字符型 array 构成的 linked list 组成的。 --- Go 语言之父 Rob Pike Go 语言里面的数组其实很不常用,这是因为数组是定长的静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不能相互转换相互赋值,用起来多有不方便之处。 切片是动态的数组,是可以扩充内容增加长度的数组。当长度不变时,它用起来就和普通数组一样。当长度不同时,它们也属于相同的类型,之间可以相互赋值。这就决定了数组的应用领域都广泛地被切片取代了。 不过也不可以小瞧数组,在切片的底层实现中,数组是切片的基石,是切片的特殊语法隐藏了内部的细节,让用户不能直接看到内部隐藏的数组。切片不过是数组的一个包装,给顽固的数组装上了灵活的翅膀,让石头也可以展翅飞翔。 仅仅是上面纯文字的说明,读者肯定会感觉很懵。下面让我们来看具体的实例。 数组变量的定义 我们先试一下只申明类型,不赋初值。这时编译器会给数组默认赋上「零值」。数组的零值就是所有内部元素的零值。 package mainimport "fmt"func main() {var a [9]int fmt.Println(a) } ------------ [0 0 0 0 0 0 0 0 0] 下面我们看看另外三种变量定义的形式, 效果都是一样的 package mainimport "fmt"func main() {var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} c := [8]int{1, 2, 3, 4, 5, 6, 7, 8} fmt.Println(a) fmt.Println(b) fmt.Println(c) } --------------------- [1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 10] [1 2 3 4 5 6 7 8] 数组的访问 接下来我们使用下标来简单操作一下数组,这个数组里存的是数字的平方值 package mainimport "fmt"func main() {var squares [9]intfor i := 0; i < len(squares); i++ { squares[i] = (i + 1) * (i + 1) } fmt.Println(squares) } -------------------- [1 4 9 16 25 36 49 64 81] 数组的下标越界检查(高阶知识) 上面的代码中我们注意到可以使用内置函数 len() 来直接获取数组的长度。数组的长度是编译期确定的,当我们使用 len() 函数访问数组的长度属性时,编译器在背后偷偷把它替换成了整数值。 package mainimport "fmt"func main() {var a = [5]int{1,2,3,4,5} a[101] = 255 fmt.Println(a) } ----- ./main.go:7:3: invalid array index 101 (out of bounds for 5-element array) 上面的代码运行结果说明了 Go 语言会对数组访问下标越界进行编译器检查。有一个重要的问题是,如果下标是一个变量,Go 是如何检查下标越界呢?变量需要在运行时才可以决定是否越界,Go 是如何办到的呢? package mainimport "fmt"func main() {var a = [5]int{1,2,3,4,5}var b = 101 a[b] = 255 fmt.Println(a) } ------------panic: runtime error: index out of range goroutine 1 [running]: main.main() /Users/qianwp/go/src/github.com/pyloque/practice/main.go:8 +0x3d exit status 2 答案是 Go 会在编译后的代码中插入下标越界检查的逻辑,所以数组的下标访问效率是要打折扣的,比不得 C 语言的数组访问性能。 数组赋值 同样的子元素类型并且是同样长度的数组才可以相互赋值,否则就是不同的数组类型,不能赋值。数组的赋值本质上是一种浅拷贝操作,赋值的两个数组变量的值不会共享。 package mainimport "fmt"func main() {var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}var b [9]int b = a a[0] = 12345 fmt.Println(a) fmt.Println(b) } -------------------------- [12345 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9] 从上面代码的运行结果中可以看出赋值后两个数组并没有共享内部元素。如果数组的长度很大,那么拷贝操作是有一定的开销的,使用的时候一定需要注意。下面我们尝试使用不同长度的数组赋值会有什么结果 package mainimport "fmt"func main() {var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}var b [10]int b = a fmt.Println(b) } -------------------------- ./main.go:8:4: cannot use a (type [9]int) as type [10]int in assignment 可以看出不同长度的数组之间赋值是禁止的,因为它们属于不同的类型。 数组的遍历 数组除了可以使用下标进行遍历之外,还可以使用 range 关键字来遍历,range 遍历提供了下面两种形式。 package mainimport "fmt"func main() {var a = [5]int{1,2,3,4,5}for index := range a { fmt.Println(index, a[index]) }for index, value := range a { fmt.Println(index, value) } } ------------0 11 22 33 44 50 11 22 33 44 5 考虑到切片的内容太多,我们将独立一节专门讲解切片,下一节将是 Go 语言的极有价值的一节,读者一定要努力搞清楚每一个细节。 原文发布时间为: 2018-11-08 本文作者:码洞 本文来自云栖社区合作伙伴“码洞”,了解相关信息可以关注“码洞”。

优秀的个人博客,低调大师

物联网:万物皆可联 产业发展快

新科技新产业影响未来(4) 物联网(IoT,Internet of Things)是通过二维码识读设备、射频识别装置、红外感应器、全球定位系统和激光扫描器等信息传感设备,按约定协议,把任何物品与互联网相连接,进行信息交换和通信,以实现智能化识别、定位、跟踪、监控和管理的一种网络。 近年来,全球物联网产业市场规模呈现快速上升之势,已从2013年的1.9万亿美元上升至2016年的3.2万亿美元。物联网产业的发展与可用连接数密切相关。预计到2020年,全球连接数将达到260亿(智能手机90亿,可穿戴设备100亿,M2M连接70亿),潜在市场规模将超过7万亿美元。 目前,我国物联网产业空间格局已经形成分别以北京、上海、深圳、重庆为核心的环渤海、长三角、珠三角、中西部地区四大产业集聚区。由于我国拥有全球最大的电子消费市场与工业规模,物联网在我国有着光明前景。2014年我国物联网产业的市场规模达到6000亿元,预计到2018年将达到1.5万亿元人民币,年复合增长率超过25%。 ■ 物联网(IoT,Internet of Things)这一概念最早在1999年提出。美国麻省理工学院建立的“自动识别中心(Auto-ID)”,首次提出“万物皆可通过网络互联”,并阐明物联网的基本含义。早期的物联网仅是依托射频识别(RFID)技术实现物物相连的网络。随着技术和应用的发展,物联网的内涵已发生了较大变化。 2005年11月17日在突尼斯举行的信息社会世界峰会(WSIS,World Summit on the Information Society)上,国际电信联盟(ITU,International Telecommunications Union)对物联网的概念进行了认定:通过二维码识读设备、射频识别装置、红外感应器、全球定位系统和激光扫描器等信息传感设备,按约定协议,把任何物品与互联网相连接,进行信息交换和通信,以实现智能化识别、定位、跟踪、监控和管理的一种网络。 需要指出的是,物联网并非单独建立一张网络,而是在现有网络基础上的拓展和延伸。就像互联网和移动互联网通过PC、平板电脑、智能手机已经实现人与人之间的互联一样,未来将能实现一切硬件的互联。因此,简单地说,物联网是在原有网络基础上,对任何客观物体网络化和智能化的过程。 如果说第一代互联网技术实现的是计算机之间的互联并成功搭建了虚拟网上世界,第二代互联网技术是通过移动互联网实现了人与人之间的互联,那么未来的互联网技术要实现的是所有物品的互联,彻底打通虚拟世界与现实世界之间的壁垒。 ■ 近年来,全球物联网产业市场规模呈现快速上升之势,已从2013年的1.9万亿美元上升至2016年的3.2万亿美元。物联网产业的发展与可用连接数密切相关。当前全球可用连接数约为90亿,其中物联网连接不到一半。预计到2020年,全球连接数将达到260亿(智能手机90亿,可穿戴设备100亿,M2M连接70亿),潜在市场规模将超过7万亿美元。 物联网涵盖了整个国民经济的方方面面,其中九个主要应用领域市场产值最大,分别为:智能汽车,包括无人驾驶与车联网等;智慧城市,包括公共健康与交通运输等;智慧物流,包括智慧运输与商业导航等;智能护理,包括辅助健康与健身等;智慧办公,包括运营最优化与运营安全等;智慧零售,包括自动结账等;智慧工厂,包括智能操作与设备最佳化等;智慧能源,包括能源互联网等;智慧家庭,包括家事自动化与家庭安全等。 ■ 物联网的建立离不开传感器应用的普及。近年来,内置传感器的新硬件纷来沓至。从VR/AR头戴式设备、智能手环、智能手表到无人驾驶汽车、无人机、工业互联网,一个全新的智能世界正向我们走来。尽管这些新硬件的销售数量级并不能算大,而且都是独立网络连接,有些甚至还可以说是“中看不中用”,但这却意味着传感器的子网络点状扩张已经开始。假以时日,各类硬件的子网络融合成一个大网络可以期待。随着基础网络容量提升和各种设备的智能化,物联网产业将在未来几年迎来快速发展期。 物联网服务对象可划分为政府、企业、消费者三类,并围绕这三类不同群体衍生出多样化应用,创造巨大社会价值。 1.面向政府的物联网应用涵盖公共资源管理、智能交通、平安城市、智慧政务等。目前,中国、美国等主要国家政府对物联网的诉求主要都是为了解决日益紧张的能源消耗、污染问题、城市安全问题,因此,未来智能交通、智能抄表、智能路灯等涉及城市生活方面的相关应用将是发展重点。 例如,瑞典政府实施智慧交通计划就是在车上装上电子车牌,和交通大数据平台连通,实现实时智能交通管理和拥堵费收取,并取得了显著成效。整个斯德哥尔摩地区交通拥堵降低了20%-25%,交通排队时间下降30%-50%,废气排放减少2.5%,二氧化碳等温室气体排放量下降了40%。 2.面向企业的物联网应用包括工业物联网、机器学习和人工智能等。企业可将设备连接到云端进行统一管理,并从生产制造、物流、销售、售后数据中寻找提升运营效率、降低成本、提升销量等重要商业问题。 例如,春秋航空通过使用GE的远程诊断探测,年节省维修费用超过21万美元,并且避免了数次计划外的发动机拆卸和停飞待用;风力涡轮机制造商Vestas通过对天气数据及客户涡轮仪表采集的数据进行交叉分析,可及时对风力涡轮机布局进行改善,从而增加了风力涡轮机的电力输出水平,延长了服务期限。 未来,工业物联网可进一步进化到借助机器学习来解决问题。例如,Able Cloud通过3个月的机器学习训练饮水机自动加热、保温和断电,达到了饮水机在“学会”满足办公室人员喝水需求的同时,还能节约30%的能源。 3.面向消费者的物联网涉及车联网、可穿戴设备、智能家居和复杂娱乐等。目前,车联网以导航、远程信息采集、车载系统升级为主。 未来随着车载娱乐系统、ADAS、无人驾驶的普及,车载智能硬件有望实现联网一体化管理。而在智慧社区和智能家居领域,平安社区和平安家庭有望率先取得突破。原有小区视频监控系统可扩展为物联网平台,结合家庭传感器、可穿戴设备、社区出入口管理系统,为现代化的社区管理提供基础设施解决方案。在发生非法入侵,火灾、燃气泄漏等异常情况时,智能传感器会发出报警信号,并通过家庭中心将信号传至小区物业管理中心、报警中心和居民手机上,通知安保人员及时赶到现场处理。 ■ 物联网产业链由感知层、网络层和应用层三大领域组成。 1.感知层:主要包括信息感知,识别物体采集信息,是物联网识别物体和信息的来源,也是其上游产业。感知层是由各种半导体传感设备及其网络网格组成,由传感器、条形码、射频识别标签以及卫星定位系统等构成的感知终端。感知层是竞争性领域。由于各个行业应用物联网模式不同,对其传感器和终端设备的需求差别较大,也对产品生产提出了较高要求。 2.网络层:主要实现物联网的数据传输和处理感知层获取的信息,是物联网的中游产业。物联网所包括的网络有互联网、局域网、城域网、外网、网络系统平台等。网络层属于寡头竞争的垄断性行业,其垄断性不仅体现在对其建设过程,而且在其运营环节也呈现不同程度的垄断。 3.应用层:这个层次是物联网的应用体系,主要作用是把物联网和终端连接起来,实现物联网真正意义上的虚拟世界与物理世界的无缝连接,达到名副其实的物物相连世界,是物联网的下游产业。应用层是市场差别较大,市场高度细分的产业组织模式。这一领域主要面向中小企业、家庭及个人服务,包括多样化的服务模式和个性化服务方式。用户的经营多具有分散性,因此在该领域物联网应用差别较大,服务市场细分程度较高。 就发展趋势看,物联网产业链的三大领域的增长路径并不一致。感知层传统领域潜力有限。据Gartner预计,2019年物联网半导体行业市场总额将达到435亿美元。其中,处理设备市场规模248亿美元,传感器市场规模100亿美元,通信芯片市场规模86亿美元。在传感器、RFID等领域,国内厂商基本没有进入高端市场,缺乏竞争壁垒导致其未来盈利窗口期较短,如不能切入高端领域,未来增长潜力有限。 从目前看,感知层领域内智能识的发展空间较大。今后小型化智能设备的普及将会带来交互方式的改变,而以图像和语音识别为代表的智能识别,或将成为物联网主流数据采集方式。网络层中运营商红利较大。当前普遍使用的蜂窝网技术优势在于实时性、安全性、兼容性、移动性较好,而Wi-Fi技术优势在于成本较低。今后,Wi-Fi与蜂窝式技术将会越来越朝向共同合作的应用模式,两种技术可能会成为预期在2020年开始被采用的5G蜂窝标准的组成部分。无论使用哪种技术,运营商的规模红利在短期内都难以撼动。 应用层中的物联网平台今后有望成为产业链核心。随着物联网在不同场景下实现大规模应用,感知层采集的数据量越来越大、数据类型越来越丰富、数据结构也会越来越复杂,数据分析的重要性将愈加凸显。此时,效聚资源、打造生态、参与应用分层的物联网平台优势将不断显现。 目前,互联网巨头与创业公司直接布局平台领域。在平台服务领域,大型互联网、IT公司由于拥有殷实的资金积累、广阔的用户基础和雄厚的网络开发实力,往往率先强占了先机。国内百度、腾讯、阿里、小米等互联网巨头都拥有自己的物联网开发服务平台,通常在其云计算业务上构建起来。同时,创业公司也凭借其对细分领域的敏感性、专业性迅速在平台领域占得一席之地,例如机智云、KII都是国内智能家居等领域比较著名的专门提供应用开发服务的创业型企业。 ■ 如今,物联网技术层面正日趋成熟,但稳定性还存在瑕疵。限制物联网产业发展的问题主要有三个。 一是基础网络能力不足。物联网的连接对象是超大规模的智能设备,因此需要稳定、持续的网络环境,对网络覆盖、时延和终端设备功耗要求较高。然而,目前4G网络主要是为大流量和按需响应的人-人通信设计,难以满足物联网构架所需。 二是终端设备价格较高。由于产品成熟有过程,价格下降需时间,当前VR、AR、无人机、智能手表、4G模组等终端设备,价格依然相对较高。主流VR设备动辄数千元;4G模组成本约为60元,经渠道商加价后将超过100元。也许对于一些技术发烧友或尝鲜者来说,VR这类产品的高价可以容忍,但对于普通消费者和企业,却很难在高成本面前选择淘汰现有设备。 三是产业链整合度还不够。物联网产业链发展仍呈现碎片化,没有形成统一的标准,也没有强力的产业链整合者。物联网的实现不仅是要设备可连接互联网,也要设备与设备之间可以无缝连接,但如今,设备间的“互操作性”还远未形成规模化。 针对上述限制物联网产业发展的三个问题,国内外科技型巨头不断进行技术创新与市场整合,取得了显著进展,有望在未来几年内彻底解决。 1.NB-IoT为代表的LPWA技术引领连接数迅猛增长。以基于蜂窝的窄带物联网(Narrow Band Internet of Things, NB-IoT)为代表的低功耗广域技术(Low Power Wide Area,LPWA)连接技术解决了移动物联网普及的障碍,LPWA大规模部署后将促进物联网连接数迅猛增长。 相比传统短距离通信技术和已有蜂窝通信技术,以NB-IoT为代表的LPWA具有四大明显优势,非常适合需要远距离数据传输、通信数据量少、需电池供电长久运行的物联网应用场景:一是广覆盖。NB-IoT的覆盖比传统GSM网络好20个DB。如果按照覆盖面积计算,一个基站可以提供10倍的面积覆盖。二是海量联接。200KHz频率下面,借助NB-IoT一个基站可以提供10万个连接。三是低功耗。NB-IoT通讯模组电池可以十年独立工作,而不需要充电。四是低成本。NB-IoT模组的成本小于5美金,且NB-IoT基于蜂窝网络,可直接部署于现有的GSM网络、UMTS网络或LTE网络,运营商部署成本较低、将实现向4.5G平滑升级。 2.技术创新推动元器件成本下降。随着材料科学、纳米技术、微电子等领域前沿技术的突破。智能化技术实现了多种传感功能与数据处理、存储、双向通信等的集成,不但成本逐年下降,还实现了高精度的信息采集、可数据存储和通信、编程自动化和功能多样化。传感器和WiFi模组向智能化、微小化、多样化迈进,生产规模不断扩大。 例如,集成微电子机械加工技术的日趋成熟推动MEMS传感器将半导体加工工艺(如氧化、光刻、扩散、沉积和蚀刻等)引入传感器的生产制造,实现了规模化生产。 3.巨头布局加速产业链整合。尽管离物联网的真正实现还较远,但目前行业快速扩散的势头已十分明朗,主要IT巨头们正在加紧布局。2016年2月,思科宣布以14亿美元收购物联网平台供应商Jasper Technologies,这标志着顶级企业级软件公司致力于主导企业物联网领域的创新。英特尔和高通也正全面向物联网一体化解决方案商转型,GE和微软则开始重点布局工业物联网,AT&T则加大车联网投入力度。 以美国最大电信运营商AT&T为例,其近年来致力于将自己在通信领域的领先地位拓展到物联网。该公司表示截至2015年三季度,他们已经有2500万个物联网终端,其中600万个是汽车。AT&T为车联网用户提供免费或付费的内容服务,包括视频节目、游戏以及儿童教学等软件应用。 ■ 我国物联网产业起步晚于美国,但至今与美国差距相差不大。与美国市场自发为主、政府引导为辅不同,我国物联网产业发展主要依靠政府的战略性推动,因此能在短期内快速迎头赶上。 2008年前是我国物联网产业的学习研究阶段。一些先行企业在在传感器及RFID领域进行了初步研究,并推出了一些初级产品。 2008-2013年是我国物联网产业的政策推动阶段。2008年,国务院提出了“感知中国”设想,中央和地方政府对物联网行业在资金和政策上均给予了大量的支持。无锡市率先建立了“感知中国”研究中心。2012年工信部制定了《物联网“十二五”发展规划》,提出重点培养物联网产业10个聚集区和100个骨干企业,实现产业链上下游企业的汇集和产业资源整合。 2013年2月,国务院发布《关于推进物联网有序健康发展的指导意见》,针对物联网发展面临的突出问题以及长远发展的需要,从全局性和顶层设计的角度进行了系统考虑,明确以智慧城市成为重要载体,在农业、交通、物流等领域初步进行推广。自此,我国物联网产业开始步入发展的快车道。 2014年后,我国物联网产业步入应用推广阶段。国内物联网不断出现设备制造业与物联网、互联网融合,创新产品和服务新模式的现象。家电行业借力物联网技术,已经率先开展拓展价值空间并改善产品服务的模式探索,进军智能家居。2014年3月,美的与阿里巴巴宣布双方将共同构建基于阿里云的物联网开放平台,实现家电产品的连接、对话和远程控制,并为第三方应用提供标准API接口,打造一个开放的智能物联网平台,未来将可支持更多类型设备的接入。 除了国家层面出台相关政策,各地政府也针对物联网产业制定了中长期的规划与战略,以期通过物联网产业发展带动地方产业转型升级。不少地方政府也出台物联网专项规划、行动方案和发展意见,从土地使用、基础设施配套、税收优惠、核心技术和应用领域等多个方面为物联网产业的发展提供政策支持。从重点区域物联网战略规划政策中可以看出,我国的东部和南部由于自身信息化程度较高,信息化基础设施建设较为完善,在推进物联网建设过程中,推进速度较快,发展阻力较小。 目前,我国物联网产业空间格局已经形成分别以北京、上海、深圳、重庆为核心的环渤海、长三角、珠三角、中西部地区四大产业集聚区。其中,环渤海以北京为核心,打造物联网产业研发、设计、运营和公共服务平台的龙头区域;长三角区域以上海和无锡为双核,是我国起步最早,产业规模最大的区域。无锡作为“国家传感网创新示范区”,集聚了大批物联网龙头企业,在技术研发、产业化与应用推广方面起到示范作用;珠三角区域成长为物联网产业制造、软件研发和系统集成的重要基地;而中西部区域在软件、信息服务、传感器等领域发展迅猛,成为第四大产业基地。产业发展聚集区的形成,能够促进产业集群和联动效应,推动产业规模化的形成,对于产业成长具有积极意义。 ■ 由于我国拥有全球最大的电子消费市场与工业规模,物联网在我国有着光明前景。根据赛迪智库的数据,2014年我国物联网产业的市场规模达到6000亿,而到2018年将达到1.5万亿元人民币,年复合增长率超过25%,充分展现出强劲发展势头。 随着中国已成为全球ICT类终端设备的生产与研发基地,或许中国企业将能从物联网尤其是工业物联网中和车联网获得比国外巨头更大的机会。据Accenture预测,未来15年中国工业物联网市场规模将达到1.8万亿美元。由于中国已经成为全球最大的制造业国际,未来工业在产业过程中将产生巨大的市场空间,其潜力十足。 尽管物联网潜在市场空间在万亿量级,但目前尚未进入爆发期。现阶段物联网的瓶颈在于网络能力不足、终端成本居高不下、产业链整合度不够。随着NB-IOT的推出、终端成本逐步下降、巨头切入物联网产业链,未来2-3年内有望出现明显改善。 在感知层器件层面,随着物联网爆发放量,市场需求存在一波机会,其中智能识别发展空间最大。但我国企业普遍集中在传统领域,国内企业竞争壁垒不强,长期增长潜力并不大。目前国内代表性企业主要是依靠政府采购支撑,若“真刀真枪”地展开竞争,前景难料。网络层受益于物联网整体发展,商业模式清晰,运营商及通信连接管理平台位于核心,收益最有保障。 应用层中物联网平台处于核心,国内IT巨头多有布局,而下游细分领域分散,有望形成专注于细分领域的小型SAAS(Software-as-a-Service,软件即服务)平台。今后,我国物联网产业可以网络层和应用层中的强势企业为突破口,逐步带动上游制造环节改善,从而全面推动整个产业发展。 本文转自d1net(转载)

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册