范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

Go的两个黑魔法技巧

  作者:pedrogao,腾讯CSIG后台研发工程师Go 的两个黑魔法技巧
  最近,在写 Go 代码的时候,发现了其特别有意思的两个奇技淫巧,于是写下这篇
  文章和大家分享一下。魔法 1:调用 runtime 中的私有函数
  按照 Go 的编译约定,代码包内以 小写字母 开头的函数、变量是私有的: package test  // 私有 func abs() {}  // 公共 func Abs() {}
  对于  test  包中 abs  函数只能在包内调用,而 Abs  函数却可以在其它包中导入后使用。
  私有变量、方法的意义在于 封装 :控制内部数据、保证外部交互的一致性。
  这样既能促进系统运行的可靠性,也能减少使用者的信息负载。
  这样的规定对设计、封装良好的包是友好的,但并不是每个人都有这样的能力,另外对于一些特殊的函数,如: runtime  中的 memmove  函数,在有些场景下,确实是需要的。
  因此 Go 在程序链接阶段给开发者打开了一扇窗,即可以通过  go:linkname  指令来链接包内的私有函数。memmove
  以 memmove 为例,
  如下: func memmove(to, from unsafe.Pointer, n uintptr)
  memmove 作为 runtime 中的私有函数,用于任意数据之间的内存拷贝,无视类型信息,直接操作内存,这样的操作在 Go 中虽然是不提倡的,但是用好了,却也是一把利刃。
  新建一个 go 文件,如 runtime.go,并加上如下内容: //go:noescape //go:linkname memmove runtime.memmove //goland:noinspection GoUnusedParameter func memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
  把视角放到  go:linkname  指令上,该指令接受两个参数:memmove:当前函数名称; runtime.memmove:对应链接的函数的路径,报名+函数名。
  这样,编译器在做链接时就会将当前的 memmove 函数链接到 runtime 中的 memmove 函数, 我们就能使用该函数了。
  在平常写代码的时候,我们经常性地需要拷贝字节切片、字符串之间的数据。比如将数据从切片 1拷贝到切片 2,使用 memmove 代码如下: // runtime.go type GoSlice struct {     Ptr unsafe.Pointer     Len int     Cap int }  // runtime_test.go func Test_memmove(t *testing.T) {  src := []byte{1, 2, 3, 4, 5, 6}  dest := make([]byte, 10, 10)   spew.Dump(src)  spew.Dump(dest)   srcp := (*GoSlice)(unsafe.Pointer(&src))  destp := (*GoSlice)(unsafe.Pointer(&dest))   memmove(destp.Ptr, srcp.Ptr, unsafe.Sizeof(byte(0))*6)   spew.Dump(src)  spew.Dump(dest) }
  字节切片([]byte)在内存中的形态如  GoSlice  结构体来所示,Len 、Cap  分别表示切片长度、容量,字段 Ptr  指向真实的字节数据。
  将两个切片的数据指针以及拷贝长度作为参数传入 memmove,数据就能从 src 拷贝到 dest。运行结果如下: === RUN   Test_memmove # 拷贝之前 ([]uint8) (len=6 cap=6) {  00000000  01 02 03 04 05 06                                 |......| } ([]uint8) (len=10 cap=10) {  00000000  00 00 00 00 00 00 00 00  00 00                    |..........| } # 拷贝之后 ([]uint8) (len=6 cap=6) {  00000000  01 02 03 04 05 06                                 |......| } ([]uint8) (len=10 cap=10) {  00000000  01 02 03 04 05 06 00 00  00 00                    |..........|
  显然,对于切片之间的数据拷贝,标准库提供的  copy  函数要更加方便一些:func Test_copy(t *testing.T) { src := []byte{1, 2, 3, 4, 5, 6} dest := make([]byte, 10, 10)   spew.Dump(src)  spew.Dump(dest)   copy(dest, src)   spew.Dump(src)  spew.Dump(dest) }
  这样也能达到一样的效果,memmove 更加适合字符串(string)和数组切片之间的数据拷贝场景,如下: // runtime.go type GoString struct {     Ptr unsafe.Pointer     Len int }  // runtime_test.go func Test_memmove(t *testing.T) {  str := "pedro"  // 注意:这里的len不能为0,否则数据没有分配,就无法复制  data := make([]byte, 10, 10)  spew.Dump(str)  spew.Dump(data)   memmove((*GoSlice)(unsafe.Pointer(&data)).Ptr, (*GoString)(unsafe.Pointer(&str)).Ptr,   unsafe.Sizeof(byte(0))*5)  spew.Dump(str)  spew.Dump(data) }
  类似地, GoString  是字符串在内存中的表达形态,通过 memmove 函数就能快速的将字符数据从字符串拷贝到切片,反之亦然,运行结果如下:# 拷贝之前 (string) (len=5) "pedro" ([]uint8) (len=10 cap=10) {  00000000  00 00 00 00 00 00 00 00  00 00                    |..........| } # 拷贝之后 (string) (len=5) "pedro" ([]uint8) (len=10 cap=10) {  00000000  70 65 64 72 6f 00 00 00  00 00                    |pedro.....| } growslice
  切片是 Go 中最常用的数据结构之一,对于切片扩容,Go 只提供了  append  函数来隐式的扩容,但内部是通过调用 runtime 中的 growslice
  函数来实现的:func growslice(et *_type, old slice, cap int) slice
  growslice 函数接受 3 个参数: et:切片容器中的数据类型,如 int, _type  可以表示 Go 中的任意类型;old:旧切片; cap:扩容后的切片容量。
  扩容成功后,返回新的切片。
  同样地,使用 go:linkname 来链接 runtime 中的 growslice 函数,如下:// runtime.go type GoType struct {  Size       uintptr  PtrData    uintptr  Hash       uint32  Flags      uint8  Align      uint8  FieldAlign uint8  KindFlags  uint8  Traits     unsafe.Pointer  GCData     *byte  Str        int32  PtrToSelf  int32 }  // GoEface 本质是 interface type GoEface struct {  Type  *GoType  Value unsafe.Pointer }  //go:linkname growslice runtime.growslice //goland:noinspection GoUnusedParameter func growslice(et *GoType, old GoSlice, cap int) GoSlice
  growslice  函数的第一个参数 et  实际是 Go 对所有类型的一个抽象数据结构——GoType 。
  这里引入了 Go 语言实现机制中的两个重要数据结构: GoEface:empty interface,即 interface{},空接口; GoType:Go 类型定义数据结构,可用于表示任意类型。
  关于 GoEface、GoIface、GoType、GoItab 都是 Go 语言实现的核心数据结构,这里的内容很多,感兴趣的可以参考这里 。
  这样,我们就能通过调用  growslice  函数来对切片进行手动扩容了,如下:// runtime.go func UnpackType(t reflect.Type) *GoType {  return (*GoType)((*GoEface)(unsafe.Pointer(&t)).Value) }  // runtime_test.go func Test_growslice(t *testing.T) {  assert := assert.New(t)   var typeByte = UnpackType(reflect.TypeOf(byte(0)))   spew.Dump(typeByte)   dest := make([]byte, 0, 10)   assert.Equal(len(dest), 0)  assert.Equal(cap(dest), 10)   ds := (*GoSlice)(unsafe.Pointer(&dest))  *ds = growslice(typeByte, *ds, 100)   assert.Equal(len(dest), 0)  assert.Equal(cap(dest), 112) }
  由于  growslice  的参数et 类型在 runtime 中不可见,我们重新定义了 GoType  来表示,
  并且通过反射的机制来拿到字节切片中的 GoType,然后调用 growslice 完成扩容工作。
  运行程序: --- PASS: Test_growslice (0.00s) PASS
  注意一个点,growslice 传入的 cap 参数是  100 ,但是最后的扩容结果却是 112 ,这个是因为 growslice 会做一个 roundupsize  处理,感兴趣的同学可以参考这里 。魔法 2:调用 C/汇编函数
  下面,我们再来看 Go 的另外一个更加有趣的黑魔法。 cgo
  通过 cgo,我们可以很方便地在 Go 中调用 C 代码,如下: /* #include  #include   static void* Sbrk(int size) {  void *r = sbrk(size);  if(r == (void *)-1){     return NULL;   }  return r; } */ import "C"  import (  "fmt" )  func main() {  mem := C.Sbrk(C.int(100))  defer C.free(mem)  fmt.Println(mem) }
  运行程序,会得到如下输出: 0xba00000
  cgo 是 Go 与 C 之间的桥梁,让 Go 可以享受 C 语言强大的系统编程能力,比如这里的  sbrk  会直接向
  进程申请一段内存,而这段内存是不受 Go GC 的影响的,因此我们必须手动地释放(free)掉它。
  在一些特殊场景,比如全局缓存,为了避免数据被 GC 掉而导致缓存失效,那么可以尝试这样使用。
  当然,这还不够 tricky,别忘了,C 语言是可以直接内联汇编的,同样地,我们也可以在 Go 中内联汇编
  试试,如下: /* #include   static int Add(int i, int j) {   int res = 0;   __asm__ ("add %1, %2"     : "=r" (res)     : "r" (i), "0" (j)   );   return res; } */ import "C" import (  "fmt" )  func main() {  r := C.Add(C.int(2022), C.int(18))  fmt.Println(r) }
  运行程序,可以得到如下输出: 2040
  cgo 虽然给了我们一座桥梁,但付出的代价也不小,具体的缺点可以参考这里。
  对 cgo 感兴趣的同学可以参考这里 。 汇编isspace
  那么有没有一种方式可以回避掉 cgo 的缺点,答案自然是可以的。
  这个方式其实很容易想到:不使用 cgo,而是使用 plan9,也就是 Go 支持的汇编语言。
  当然我们不是直接去写汇编,而是将 C 编译成汇编,然后再转化成 plan9 与 .go 代码一起编译。
  编译的过程如下图所示:
  而且 C 本身就是汇编的高级抽象,作为目前最强劲性能的存在,这种方式不仅回避了 cgo 的性能问题,
  反而将程序性能提高了。过程如下:
  首先,我们定义一个简单的 C 语言函数  isspace (判断字符为空):// ./inner/op.h #ifndef OP_H #define OP_H  char isspace(char ch);  // ./inner/op.c #include "op.h"  char isspace(char ch) {     return ch == " " || ch == "r" || ch == " " | ch == "	"; }
  然后,使用  clang  将其编译为汇编(注意:是 clang):$ clang -mno-red-zone -fno-asynchronous-unwind-tables -fno-builtin -fno-exceptions  -fno-rtti -fno-stack-protector -nostdlib -O3 -msse4 -mavx -mno-avx2 -DUSE_AVX=1   -DUSE_AVX2=0 -S ./inner/*.c
  编译成功后,会在 inner 文件夹下生成一个  op.s  汇编文件,大致如下:	.section	__TEXT,__text,regular,pure_instructions 	.build_version macos, 11, 0 	.globl	_isspace                        ## -- Begin function isspace 	.p2align	4, 0x90 _isspace:                               ## @isspace ## %bb.0: 	pushq	%rbp 	movq	%rsp, %rbp 	movb	$1, %al 	cmpb	$13, %dil 	je	LBB0_3
  clang 默认生成的汇编是 AT&T 格式的,这种汇编 Go 是无法编译的(gccgo 除外),因此这里有一步转换工作。
  负责将 AT&T 汇编转化成 plan9 汇编,而二者之间的语法差异其实是比较大的,因此这里借助一个转换asm2asm 工具 来完成。
  将  asm2asm  clone 到本地,然后运行:$ git clone https://github.com/chenzhuoyu/asm2asm $ ./tools/asm2asm.py ./op.s ./inner/op.s
  执行后,会报错。原因在于,Go 对于 plan9 汇编文件需要一个对应的 .go 声明文件来对应。
  我们在  ./inner/op.h  文件中定义了 isspace  函数,因此需要新建一个同名的 op.go 文件来声明这个函数://go:nosplit //go:noescape //goland:noinspection GoUnusedParameter func __isspace(ch byte) (ret byte)
  然后再次运行 asm2asm 工具来生成汇编: $ ./tools/asm2asm.py ./op.s ./inner/op.s  $ tree .  . |__ inner |   |__  op.c |   |__ op.h |   |__ op.s |__ op.go |__ op.s |__ op_subr.go
  asm2asm 会生成两个文件: op.s  和 op_subr.go :op.s:翻译而来的 plan9 汇编文件; op_subr.go:函数调用辅助文件。
  生成后,op.go 中的  __isspace  函数就能顺利的链接上对应的汇编代码,并运行,如下:func Test___isspace(t *testing.T) {  type args struct {   ch byte  }  tests := []struct {   name    string   args    args   wantRet byte  }{   {    name:    "false",    args:    args{ch: "0"},    wantRet: 0,   },   {    name:    "true",    args:    args{ch: " "},    wantRet: 1,   },  }  for _, tt := range tests {   t.Run(tt.name, func(t *testing.T) {    if gotRet := __isspace(tt.args.ch); gotRet != tt.wantRet {     t.Errorf("__isspace() = %v, want %v", gotRet, tt.wantRet)    }   })  } }  // output === RUN   Test___isspace === RUN   Test___isspace/false === RUN   Test___isspace/true --- PASS: Test___isspace (0.00s)     --- PASS: Test___isspace/false (0.00s)     --- PASS: Test___isspace/true (0.00s) PASS
  __isspace 顺利运行,并通过了单测。 u32toa_small
  一个 isspace 函数有些简单,无法完全发挥出汇编的能力,下面我们来看一个稍微复杂一点的例子:将整数转化为字符串。
  在 Go 中,整数转化为字符串的方式有多种,比如说: strconv.Itoa  函数。
  这里,我选择用 C 来写一个简单的整数转字符串的函数: u32toa_small ,然后将其编译为汇编代码供 Go 调用,并看看二者之间的性能差异。
  u32toa_small 的实现也比较简单,使用查表法(strconv.Itoa 使用的也是这种方法),如下: #include "op.h"  static const char Digits[200] = {     "0", "0", "0", "1", "0", "2", "0", "3", "0", "4", "0", "5", "0", "6", "0", "7", "0", "8", "0", "9",     "1", "0", "1", "1", "1", "2", "1", "3", "1", "4", "1", "5", "1", "6", "1", "7", "1", "8", "1", "9",     "2", "0", "2", "1", "2", "2", "2", "3", "2", "4", "2", "5", "2", "6", "2", "7", "2", "8", "2", "9",     "3", "0", "3", "1", "3", "2", "3", "3", "3", "4", "3", "5", "3", "6", "3", "7", "3", "8", "3", "9",     "4", "0", "4", "1", "4", "2", "4", "3", "4", "4", "4", "5", "4", "6", "4", "7", "4", "8", "4", "9",     "5", "0", "5", "1", "5", "2", "5", "3", "5", "4", "5", "5", "5", "6", "5", "7", "5", "8", "5", "9",     "6", "0", "6", "1", "6", "2", "6", "3", "6", "4", "6", "5", "6", "6", "6", "7", "6", "8", "6", "9",     "7", "0", "7", "1", "7", "2", "7", "3", "7", "4", "7", "5", "7", "6", "7", "7", "7", "8", "7", "9",     "8", "0", "8", "1", "8", "2", "8", "3", "8", "4", "8", "5", "8", "6", "8", "7", "8", "8", "8", "9",     "9", "0", "9", "1", "9", "2", "9", "3", "9", "4", "9", "5", "9", "6", "9", "7", "9", "8", "9", "9", };  // < 10000 int u32toa_small(char *out, uint32_t val) {     int      n  = 0;     uint32_t d1 = (val / 100) << 1;     uint32_t d2 = (val % 100) << 1;      /* 1000-th digit */     if (val >= 1000) {         out[n++] = Digits[d1];     }      /* 100-th digit */     if (val >= 100) {         out[n++] = Digits[d1 + 1];     }      /* 10-th digit */     if (val >= 10) {         out[n++] = Digits[d2];     }      /* last digit */     out[n++] = Digits[d2 + 1];     return n; }
  然后在 op.go 中加入对应的  __u32toa_small  函数:// < 10000 //go:nosplit //go:noescape //goland:noinspection GoUnusedParameter func __u32toa_small(out *byte, val uint32) (ret int)
  使用 clang 重新编译 op.c 文件,并用 asm2asm 工具来生成对应的汇编代码(节选部分): _u32toa_small: 	BYTE $0x55  // pushq        %rbp 	WORD $0x8948; BYTE $0xe5  // movq         %rsp, %rbp 	MOVL SI, AX 	IMUL3Q $1374389535, AX, AX 	SHRQ $37, AX 	LEAQ 0(AX)(AX*1), DX 	WORD $0xc06b; BYTE $0x64  // imull        $100, %eax, %eax 	MOVL SI, CX 	SUBL AX, CX 	ADDQ CX, CX 	CMPL SI, $1000 	JB LBB1_2 	LONG $0x60058d48; WORD $0x0000; BYTE $0x00  // leaq         $96(%rip), %rax  /* _Digits(%rip) */ 	MOVB 0(DX)(AX*1), AX 	MOVB AX, 0(DI) 	MOVL $1, AX 	JMP LBB1_3
  然后在 Go 中调用该函数: func Test___u32toa_small(t *testing.T) {  var buf [32]byte  type args struct {   out *byte   val uint32  }  tests := []struct {   name    string   args    args   wantRet int  }{   {    name: "9999",    args: args{     out: &buf[0],     val: 9999,    },    wantRet: 4,   },   {    name: "1234",    args: args{     out: &buf[0],     val: 1234,    },    wantRet: 4,   },  }  for _, tt := range tests {   t.Run(tt.name, func(t *testing.T) {    got := __u32toa_small(tt.args.out, tt.args.val)    assert.Equalf(t, tt.wantRet, got, "__u32toa_small(%v, %v)", tt.args.out, tt.args.val)    assert.Equalf(t, tt.name, string(buf[:tt.wantRet]), "ret string must equal name")   })  } }
  测试成功,__u32toa_small 函数不仅成功运行,而且通过了测试。
  最后,我们来做一个性能跑分看看 __u32toa_small 和 strconv.Itoa 之间的性能差异: func BenchmarkGoConv(b *testing.B) {  val := int(rand.Int31() % 10000)  b.ResetTimer()  for n := 0; n < b.N; n++ {   strconv.Itoa(val)  } }  func BenchmarkFastConv(b *testing.B) {  var buf [32]byte  val := uint32(rand.Int31() % 10000)  b.ResetTimer()  for n := 0; n < b.N; n++ {   __u32toa_small(&buf[0], val)  } }
  使用  go test -bench  运行这两个性能测试函数,结果如下:BenchmarkGoConv BenchmarkGoConv-12     60740782         19.52 ns/op  BenchmarkFastConv BenchmarkFastConv-12    122945924          9.455 ns/op
  从结果中,可以明显看出 __u32toa_small 优于 Itoa,大概有一倍的提升。 总结
  至此,Go 的两个黑魔法技巧已经介绍完毕了,感兴趣的同学可以自己实践看看。
  Go 的黑魔法一定程度上都使用了 unsafe 的能力,这也是 Go 不提倡的,当然使用 unsafe 其实就和普通的 C 代码编写一样,因此也无需有太强的心理负担。
  实际上,上述的两种方法都被 sonic 用在了生产环境上,而且带来的很大的性能提升,节约大量资源。
  因此,当 Go 现有的标准库无法满足你的需求时,不要受到语言本身的限制,而是用虽然少见但有效的方式去解决
  它。
  希望上面的两个黑魔法能带你对 Go 不一样的认识。

手机内卷化加重缺席1296天的魅蓝还有未来吗?今年国产手机市场内卷化严重,尤其连iPhone13都全系降价313左右,进一步加大了市场争夺战的力度。顶级大厂都在卯足劲大干快上,而中小品牌商还有多少机会呢?前段时间,魅族科技突然双11冲刺国内市场千万销量realme底气何在?realme现如今,国内手机市场的格局已经发生了变化,从前两年的一家独大,已变成了如今的百花齐放,不少新品牌开始走近人们的视野,与老牌厂商开启了你增我涨的角逐,其中表现亮眼的要属r看完陶琳才知道,马斯克为什么要解散特斯拉公关团队之前马斯克解散特斯拉公关团队,有人觉得鲁莽,有人觉得欠妥。普遍的认知是公关团队还是不可或缺的。但这几天的事一出来,忽然发现马斯克简直太明智了。好的公关团队,既是优化剂,又是灭火器。145元的苹果擦屏布,性能极佳,考验信仰的时候到了苹果发布会苹果发布会简直令人尖叫,M1ProM1Max两款神级处理器加持的MacBookPro实在太强大了,最高性能居然能达到英特尔i9处理器的两倍,这让牙膏厂情何以堪?不过,有细先拖后分,玖富三套方案背后,都是巨大阴谋有朋友投了玖富,但一直并不关心网贷,所以玖富刚出事的时候,他的想法是两三年才能收回本金,太可气了。前两天这朋友又问,玖富七折或者八折是不是可以下车?如果可以,七折我就抢先下车了。当员工私下辱骂女记者,理想汽车是否要道歉?刚看到有人朋友圈发图,李想询问理想汽车是否应该公开道歉。第一反应是理想道歉个啥,难道特斯拉女车主是他们送车展去的?这锅大到蔚来都要去报案了,还有哪家敢往上凑。再一看图,才明白,这是急了,腾讯低俗短视频是猪食,字节你封禁搞垄断敌人的敌人就是朋友,尽管可能也是自己的对手。昨天也就是6月3日,优酷总裁樊路远爱奇艺CEO龚宇和腾讯在线视频首席执行官孙忠怀难得地遇到了一起,提前商量过没有不知道,但现场口径非常一阿里受害女员工的心,海底针跟公布吴亦凡的瓜一样,济南警方特意选了周六晚上,大家都去过七夕,一个传播低点来公布了阿里的公告。事实证明,真正有传播点的内容不用看啥传播周期,每个群立马炸了。警方蓝底白字的公告有点小米平板1999,不如再等等还有更多安卓平板在八月十号的小米发布会上雷军发布了小米平板5,这与上一代平板时隔了三年,在小米平板的官方微博上也是被吐槽找回账号。这条微博也是获得了极高的热度,证明消费者对平板还是有很大的热情,特不满5折多被收割,诺亚投资人高速广告牌寻难友国庆期间,很多人的注意力都被紫金矿主的大婚吸引走了,但陈景河并非真正矿主,紫金矿业是国企,这1500亿的市值并不是陈老板自己的,他连10大股东都不算,但他是管理这个1500亿的大盘普利司通搏天族Sports高性能轮胎助力玛莎拉蒂MC20亮相中国赛道近日,玛莎拉蒂携超级跑车MC20震撼登陆宁波国际赛道,作为MC20的独家轮胎合作伙伴,普利司通为玛莎拉蒂量身打造的搏天族Sports高性能轮胎也同期亮相,携手玛莎拉蒂共同见证百余媒
2021下半年买哪款手机?5款最佳机型推荐,绝对值得选择时间真的是一把杀猪刀啊,一晃就到年底了再回顾下2021年的手机市场,除了普遍涨价之外,竞争还蛮激烈的,各大厂商都推出了不少新机,最近我综合对比了一下,在4K价位,我选出了5款最值得上市公司加码固态电池研发商业化进程料早于预期12月13日,北汽蓝谷在互动平台表示,公司目前完成了第二代固态电芯开发电池系统台架测试验证和整车(ARCFOXT)搭载验证。同时,第三代固态电池研发也在规划推进中。随着固态电池产业2021款特斯拉ModelY新车商品性评价作为国内领先的第三方汽车品质评价平台,车质网基于海量的汽车产品测试样本及科学的数据模型,推出新车商品性评价栏目。每月针对数款国内上市两年以内,行驶里程不超过5000公里的在售车型,鸿蒙系统7nm麒麟芯片全面屏,跌至732元,网友还能再战2年华为自研鸿蒙系统,深受国内用户的喜爱,根据华为公布的消息,鸿蒙系统的升级用户量已经突破1。5亿,并且用户口碑也很好,余承东表示,华为手机升级鸿蒙系统后,能够将性能发挥更加极致,并且需求增长叠加技术迭代光模块有望迎来戴维斯双击国家发改委工信部发布关于振作工业经济运行推动工业高质量发展的实施方案的通知提到,在5G千兆光网等领域布局一批新型基础设施项目。另外,中国移动首发申请近日获得核准,值得一提的是,募投新能源新基建带动铜箔需求前景广阔超华科技加码铜箔产业布局12月14日晚间,超华科技(002288。SZ)发布铜箔产业基地项目投资合作协议之补充协议,对其项目公司股权架构及投融资安排进行调整,以期加快推动铜箔产业基地项目建设。据了解,今年老人家始终不习惯戴助听器怎么办?老人不愿佩戴助听器的原因很多,有的是不愿意让别人看到有的认为戴助听器说明自己老了有的是随便购买,没有经过专业验配,佩戴不适合。有的是初期佩戴,还没有适应,就放弃了有的是言语分辨能力中国未来十年,2个重要机会就在近期,人民日报刊登了题为必须实现高质量发展的文章,这不仅给中国未来十年的发展定了方向,同时也明确了股市未来的两个重要机会,一分钟给你讲清楚1科技创新被提到了前所未有的高度。主要从能用到好用自主操作系统加速前行来源人民网操作系统是计算机系统的核心,也是信息产业发展的重要基础之一。近年来,随着信息技术的高速发展,操作系统的重要性和地位日益凸显,国产操作系统在数字政府企业数字化转型等领域正得软件测试的底层逻辑是什么?事物间的共同点,就是底层逻辑。只有不同之中的相同之处变化背后不变的东西,才是底层逻辑。底层逻辑环境变量方法论所以我们要来探讨一下软件测试的底层逻辑是什么?1。对软件测试的基本认知对华为12月23日将发布全新一代MatebookXPro笔记本据悉HUAWEI余承东官宣23日将发布华为全新一代HUAWEIMatebookXPro笔记本,本次更新主打超级智慧。图片摘自网络从官宣图片解读,该笔记本这次的升级主打效率提升,屏幕