daryl

daryl 查看完整档案

杭州编辑杭州电子科技大学  |  信息安全 编辑百度  |  高级研发工程师 编辑 is.daryl.moe 编辑
编辑

2018 年上半年要做的事情:

[] 读完《现代操作系统》
[x] 读完《数据结构与算法分析(C 语言描述)》
[] 读《UNIX 环境高级编程》
[] 读《UNIX 网络编程(卷1)》
[] 尝试完成自己的一个 PHP 框架
[] 尝试实现一个 Workman
[] 至少读完一半《C++ Primer》

个人动态

daryl 赞了文章 · 2019-09-11

Phper 学 C 兴趣入门 -为什么有的字符串处理这么难

需求

假如有这样的一个需求,有个日期,想要截取获得其年份。我们用 php 可以使用explode,也可以使用strtok

$a = "2019-09-10 00:00:00";
echo strtok($a,"-"); // 2019

可能大家对strtok不太熟悉,它的作用是用-来分割$a获取子串,循环调用可以达到和explode差不多的效果。具体可以看下官方手册里面的 demo https://www.php.net/manual/zh...

实验

实验1

我之所以用strtok呢,是因为C 语言里也有这个函数,这个函数比较“怪”,每一次调用,是将字符串中找到的-替换为\0,然后返回标记字符串的首地址。

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char date[] = "2019-09-10";
    char *tmp   = strtok(date, "-");

    printf("%s,%p\n", tmp, (void *) tmp);   // 2019,0x7ffe8741bdd0
    printf("%s,%p\n", date, (void *) date); // 2019,0x7ffe8741bdd0
    printf("%d,%c\n", date[4], date[4]);    // 0,

    return 0;
}

实验2

当我们使用char指针来作为字符串的初始化时,又会是怎样呢?

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char *date = "2019-09-10";
    char *tmp  = strtok(date, "-");

    printf("%s,%p\n", tmp, (void *) tmp);   // 2019,0x7ffe8741bdd0
    printf("%s,%p\n", date, (void *) date); // 2019,0x7ffe8741bdd0
    printf("%d,%c\n", date[4], date[4]);    // 0,

    return 0;
}

运行的结果却是

Segmentation fault

原理

当我们使用指针变量作为左值,双引号字符串作为右值时,背后双引号的逻辑是:

  • 在只读区申请内存,存放字符串
  • 在字符串尾加上了'/0'
  • 返回字符串的首地址

所以char * date就在栈上存放里双引号字符串返回的首地址。当使用strtok的时候,通过实验1可以看到strtok实际是找到的字符串替换为\0,也就是说需要修改原字符串的。而该字符串是在只读区,不不能修改,所以运行出现了段错误。

反过来思考,我们 char date[]数组通过双引号初始化的时候又是什么原理,是不是也是双引号返回了常量字符串首地址,然后再通过循环一个个赋值到char数组里呢?

实验3

猜想归猜想。我们通过实验来证明下。

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char *str1  = "123";
    char str2[] = {'1','2','3'};
    char str3[] = {"123"};
    char str4[] = "123";
 
    return 0;
}

通过objdump 反汇编可以看到

$ gcc a.c
$ objdump -D a.out
00000000004004ed <main>:
  4004ed:    55                       push   %rbp
  4004ee:    48 89 e5                 mov    %rsp,%rbp
  4004f1:    89 7d cc                 mov    %edi,-0x34(%rbp)
  4004f4:    48 89 75 c0              mov    %rsi,-0x40(%rbp)
  4004f8:    48 c7 45 f8 c0 05 40     movq   $0x4005c0,-0x8(%rbp)
  4004ff:    00
  400500:    c6 45 f0 31              movb   $0x31,-0x10(%rbp)
  400504:    c6 45 f1 32              movb   $0x32,-0xf(%rbp)
  400508:    c6 45 f2 33              movb   $0x33,-0xe(%rbp)
  40050c:    c7 45 e0 31 32 33 00     movl   $0x333231,-0x20(%rbp)
  400513:    c7 45 d0 31 32 33 00     movl   $0x333231,-0x30(%rbp)
  40051a:    b8 00 00 00 00           mov    $0x0,%eax
  40051f:    5d                       pop    %rbp
  400520:    c3                       retq
  400521:    66 2e 0f 1f 84 00 00     nopw   %cs:0x0(%rax,%rax,1)
  400528:    00 00 00
  40052b:    0f 1f 44 00 00           nopl   0x0(%rax,%rax,1)

image.png

$objdump -j .rodata -d 3.out

a.out:     file format elf64-x86-64


Disassembly of section .rodata:

00000000004005b0 <_IO_stdin_used>:
  4005b0:    01 00 02 00 00 00 00 00                             ........

00000000004005b8 <__dso_handle>:
    ...
  4005c0:    31 32 33 00                                         123.

实验结论

可以看到
第一个变量(黄色框)初始化是传入了一个地址,而这个地址4005c0正是下面只读数据段里面的,我们可以看到下面4005c0 储存数据31323300十六进制对应的ascii码里面的就是123\0
第二个变量(红色框)是通过三次mov操作放到了栈上(movb表示按字节移动)。
第三个变量和第四个变量的方式一样,都是直接把字符串传递到了栈上,而不是像第一个变量那样,传递的是一个地址。

所以,用指针初始化的字符串在只读取,不能被改写;用 char 数组形式初始化的字符串,即使使用了双引号来初始化,也是在栈上,后面程序是可以改写的。

扩展

C 语言也太坑爹了,这样每个函数怎么用,我们怎么知道传入的字符串在函数内部会不会做变更呢?
其实在函数手册可以看到一些细节,比如下面的函数

char *strchr(const char *s, int c);
char *strtok(char *str, const char *delim);
char *strcat(char *dest, const char *src);

当形参为const char *的时候,说明函数不会对该段内存里的数据做变更,传入栈上、堆上、只读区的地址都行;反之,如果形参为char *就要小心了,可以认为它的意思是数组,会改变传入的“字符串”。

思考

根据我们上面分析的

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char *date = "2019";
    strcat(date, "-09-10");
    printf("%s,%p\n", date, (void *) date);

    return 0;
}

运行时肯定是Segmentation fault了,因为“2019”是存在了只读取。

如果换成下面的代码,又会怎样呢?

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char date[] = "2019";
    strcat(date, "-09-10");
    printf("%s,%p\n", date, (void *) date);

    return 0;
}

linux gcc 编译可运行,但是实际是有问题的,比如我改成

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char date[] = "2019";
    strcat(date, "-09-1000000000000000000");
    printf("%s,%p\n", date, (void *) date);

    return 0;
}

就会出现段错误,也许在你的服务器编译运行又不报错,如果不报错请增加追加字符串的长度然后尝试。(C 程序就是这么神奇,能运行不一定表示没问题)因为date初始化分配的内存不足以存放连接之后的字符串。我们改写为

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char date[11] = "2019";
    strcat(date, "-09-10");
    printf("%s,%p\n", date, (void *) date);

    return 0;
}

这样就可以正常运行了。坑爹啊,C 语言也麻烦了,一不小心就写错,怪不得 PHP 是世界上最好的语言。

安利

世上无难事只怕有心人,如果觉得想学C语言,又比较困难,不如我们一起来学,赶快上车 https://segmentfault.com/ls/1...

也欢迎大家关注我的公众号,不发骚扰,只发干货原创文章
图片描述

查看原文

赞 33 收藏 6 评论 6

daryl 回答了问题 · 2019-03-06

swoole 协程下 使用静态属性、单例 优缺点有什么?

没什么优缺点的,注意线程安全就行。。。

关注 2 回答 1

daryl 评论了文章 · 2019-03-05

使用桩件 (Stub) 解决 Laravel 单元测试中的依赖

本文是我在实践后的一点总结,难免有不妥之处。如有幸得大神路过,还望不吝赐教,小弟在此谢过了!

很早就知道有单元测试的概念,也曾尝试过,但是一直对单元测试的概念和方法,比较模糊。在听了 @vimac 大神的讲堂 PHP单元测试与测试驱动开发 后,慢慢地对单元测试和 PHPUnit 的认识清晰了起来,也开始慢慢地去实践单元测试。

Laravel 中的依赖

我们都知道,Laravel 使用了 IoC,各个模块之间也因此解耦了。而正是因为这一点,我们在 Laravel 中编写单元测试的时候,变得更加轻松了。

举个栗子

考虑以下场景。我们在开发中,可能会在控制器和模型之间加一个 Repository 来处理数据。那么我们的 Controller 就会依赖 Respository。利用 Laravel 的 IoC,我们可以定义一个 Service Provider 来集中将 Respository 注入到容器中。

假设我们现在有这样一个 Repository,里面记录了商品的信息,我们想要在 Controller 中获取某件商品信息,然后执行一些业务逻辑。

Class GoodRepository
{
    public function getGoodById($goodId)
    {
        // TODO: Get good by its id.
    }
}

class GoodController extends Controller
{
    public function show($id, GoodRepository $goodRepository)
    {
        // TODO: Do something with good info from that repository.
    }
}

// In route/api.php
Route::get('/api/good/{id}', 'GoodController@show');

// Create a RepositoriesServiceProvider in Provider/RepositoriesServiceProvider.php。
// And inject the GoodRepository into Container.
class RepositoriesServiceProvider extends ServiceProvider
{
    public function boot()
    {
    
    }
    
    public function register()
    {
        $this->app->singleton(GoodRepository::class);
    }
}

好了,我们可以发现,GoodController 是依赖 GoodRepository 的,而 GoodRepository 是依赖数据库中的数据的。可是我们在做单元测试的时候,希望尽可能少的产生依赖。所以,我们应该希望能够掌控 GoodRepository 所返回的数据。

在 Laravel,提供了 $this->get('/path/to/route'); 的方法来对 HTTP 请求进行测试。这个测试必然会涉及到刚才所提到的那些依赖,如何解决这个依赖的问题,我们可以请出我们的主角————桩件。

桩件

将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。

这是 PHPUnit 文档上 的解释。那我的理解呢,所谓的桩件,就是模拟一个依赖的类的行为,使得这个行为所做的事情在我们自己的掌控之中。比如上面的这种情况,我们希望模拟 GoodRepositorygetGoodById 方法返回与真正的返回结构相同的值,而不需要依赖外部数据源。

在 Laravel 中使用桩件

我们通过 Service Provider 注册了 GoodRepository 单例,那么按照这个思路,我们在写单元测试的时候,就可以将我们定义的桩件,注册为 GoodRepository 单例。

class GoodControllerTest extends TestCase
{
    public function testShow()
    {
        $data = []; // The data returns from GoodRepository::getGoodById.
        
        $stub = $this->createMock(GoodRepository::class);
        
        $stub->method('getGoodById')->will($this->returnValue($data));
        
        $this->app->singleton(GoodRepository::class, function () use ($stub) {
            return $stub;
        });
        
        $response = $this->get('/api/good/1');
        
        // Some assertions.
    }
}

我们通过在这里将桩件 $stub 用单例模式注册给了 Container,在调用 $this->get('/api/good/1'); 时原本在 Controller 中的 GoodRepository 依赖就变成了我们自定义的桩件 $stub。我们将 $data 定义为和返回值相同的结构,注册到桩件中。这样,所有的数据都在我们可控的范围了。

如果我们在这里不使用桩件,而是直接依赖外部(数据库)中的数据,那么如果 id 为 1 的数据被删除了,我们是不是就要改成 2 了呢?我们是不是就要重新计算数据了匹配断言了呢?这样的测试,可靠性便大大降低。

后记

任何一个可靠的系统,单元测试都是必不可少的。庆幸的是,PHPUnit 帮我们提供了好用的单元测试。本文所讲的,也只是 PHPUnit 的九牛一毛。而我自己也在慢慢摸索慢慢实践中。与君共勉。

最后还是推荐去听一下 @vimac 的讲堂 PHP单元测试与测试驱动开发,受益匪浅。

查看原文

daryl 评论了文章 · 2019-03-03

使用桩件 (Stub) 解决 Laravel 单元测试中的依赖

本文是我在实践后的一点总结,难免有不妥之处。如有幸得大神路过,还望不吝赐教,小弟在此谢过了!

很早就知道有单元测试的概念,也曾尝试过,但是一直对单元测试的概念和方法,比较模糊。在听了 @vimac 大神的讲堂 PHP单元测试与测试驱动开发 后,慢慢地对单元测试和 PHPUnit 的认识清晰了起来,也开始慢慢地去实践单元测试。

Laravel 中的依赖

我们都知道,Laravel 使用了 IoC,各个模块之间也因此解耦了。而正是因为这一点,我们在 Laravel 中编写单元测试的时候,变得更加轻松了。

举个栗子

考虑以下场景。我们在开发中,可能会在控制器和模型之间加一个 Repository 来处理数据。那么我们的 Controller 就会依赖 Respository。利用 Laravel 的 IoC,我们可以定义一个 Service Provider 来集中将 Respository 注入到容器中。

假设我们现在有这样一个 Repository,里面记录了商品的信息,我们想要在 Controller 中获取某件商品信息,然后执行一些业务逻辑。

Class GoodRepository
{
    public function getGoodById($goodId)
    {
        // TODO: Get good by its id.
    }
}

class GoodController extends Controller
{
    public function show($id, GoodRepository $goodRepository)
    {
        // TODO: Do something with good info from that repository.
    }
}

// In route/api.php
Route::get('/api/good/{id}', 'GoodController@show');

// Create a RepositoriesServiceProvider in Provider/RepositoriesServiceProvider.php。
// And inject the GoodRepository into Container.
class RepositoriesServiceProvider extends ServiceProvider
{
    public function boot()
    {
    
    }
    
    public function register()
    {
        $this->app->singleton(GoodRepository::class);
    }
}

好了,我们可以发现,GoodController 是依赖 GoodRepository 的,而 GoodRepository 是依赖数据库中的数据的。可是我们在做单元测试的时候,希望尽可能少的产生依赖。所以,我们应该希望能够掌控 GoodRepository 所返回的数据。

在 Laravel,提供了 $this->get('/path/to/route'); 的方法来对 HTTP 请求进行测试。这个测试必然会涉及到刚才所提到的那些依赖,如何解决这个依赖的问题,我们可以请出我们的主角————桩件。

桩件

将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。

这是 PHPUnit 文档上 的解释。那我的理解呢,所谓的桩件,就是模拟一个依赖的类的行为,使得这个行为所做的事情在我们自己的掌控之中。比如上面的这种情况,我们希望模拟 GoodRepositorygetGoodById 方法返回与真正的返回结构相同的值,而不需要依赖外部数据源。

在 Laravel 中使用桩件

我们通过 Service Provider 注册了 GoodRepository 单例,那么按照这个思路,我们在写单元测试的时候,就可以将我们定义的桩件,注册为 GoodRepository 单例。

class GoodControllerTest extends TestCase
{
    public function testShow()
    {
        $data = []; // The data returns from GoodRepository::getGoodById.
        
        $stub = $this->createMock(GoodRepository::class);
        
        $stub->method('getGoodById')->will($this->returnValue($data));
        
        $this->app->singleton(GoodRepository::class, function () use ($stub) {
            return $stub;
        });
        
        $response = $this->get('/api/good/1');
        
        // Some assertions.
    }
}

我们通过在这里将桩件 $stub 用单例模式注册给了 Container,在调用 $this->get('/api/good/1'); 时原本在 Controller 中的 GoodRepository 依赖就变成了我们自定义的桩件 $stub。我们将 $data 定义为和返回值相同的结构,注册到桩件中。这样,所有的数据都在我们可控的范围了。

如果我们在这里不使用桩件,而是直接依赖外部(数据库)中的数据,那么如果 id 为 1 的数据被删除了,我们是不是就要改成 2 了呢?我们是不是就要重新计算数据了匹配断言了呢?这样的测试,可靠性便大大降低。

后记

任何一个可靠的系统,单元测试都是必不可少的。庆幸的是,PHPUnit 帮我们提供了好用的单元测试。本文所讲的,也只是 PHPUnit 的九牛一毛。而我自己也在慢慢摸索慢慢实践中。与君共勉。

最后还是推荐去听一下 @vimac 的讲堂 PHP单元测试与测试驱动开发,受益匪浅。

查看原文

daryl 评论了文章 · 2019-03-02

使用桩件 (Stub) 解决 Laravel 单元测试中的依赖

本文是我在实践后的一点总结,难免有不妥之处。如有幸得大神路过,还望不吝赐教,小弟在此谢过了!

很早就知道有单元测试的概念,也曾尝试过,但是一直对单元测试的概念和方法,比较模糊。在听了 @vimac 大神的讲堂 PHP单元测试与测试驱动开发 后,慢慢地对单元测试和 PHPUnit 的认识清晰了起来,也开始慢慢地去实践单元测试。

Laravel 中的依赖

我们都知道,Laravel 使用了 IoC,各个模块之间也因此解耦了。而正是因为这一点,我们在 Laravel 中编写单元测试的时候,变得更加轻松了。

举个栗子

考虑以下场景。我们在开发中,可能会在控制器和模型之间加一个 Repository 来处理数据。那么我们的 Controller 就会依赖 Respository。利用 Laravel 的 IoC,我们可以定义一个 Service Provider 来集中将 Respository 注入到容器中。

假设我们现在有这样一个 Repository,里面记录了商品的信息,我们想要在 Controller 中获取某件商品信息,然后执行一些业务逻辑。

Class GoodRepository
{
    public function getGoodById($goodId)
    {
        // TODO: Get good by its id.
    }
}

class GoodController extends Controller
{
    public function show($id, GoodRepository $goodRepository)
    {
        // TODO: Do something with good info from that repository.
    }
}

// In route/api.php
Route::get('/api/good/{id}', 'GoodController@show');

// Create a RepositoriesServiceProvider in Provider/RepositoriesServiceProvider.php。
// And inject the GoodRepository into Container.
class RepositoriesServiceProvider extends ServiceProvider
{
    public function boot()
    {
    
    }
    
    public function register()
    {
        $this->app->singleton(GoodRepository::class);
    }
}

好了,我们可以发现,GoodController 是依赖 GoodRepository 的,而 GoodRepository 是依赖数据库中的数据的。可是我们在做单元测试的时候,希望尽可能少的产生依赖。所以,我们应该希望能够掌控 GoodRepository 所返回的数据。

在 Laravel,提供了 $this->get('/path/to/route'); 的方法来对 HTTP 请求进行测试。这个测试必然会涉及到刚才所提到的那些依赖,如何解决这个依赖的问题,我们可以请出我们的主角————桩件。

桩件

将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。

这是 PHPUnit 文档上 的解释。那我的理解呢,所谓的桩件,就是模拟一个依赖的类的行为,使得这个行为所做的事情在我们自己的掌控之中。比如上面的这种情况,我们希望模拟 GoodRepositorygetGoodById 方法返回与真正的返回结构相同的值,而不需要依赖外部数据源。

在 Laravel 中使用桩件

我们通过 Service Provider 注册了 GoodRepository 单例,那么按照这个思路,我们在写单元测试的时候,就可以将我们定义的桩件,注册为 GoodRepository 单例。

class GoodControllerTest extends TestCase
{
    public function testShow()
    {
        $data = []; // The data returns from GoodRepository::getGoodById.
        
        $stub = $this->createMock(GoodRepository::class);
        
        $stub->method('getGoodById')->will($this->returnValue($data));
        
        $this->app->singleton(GoodRepository::class, function () use ($stub) {
            return $stub;
        });
        
        $response = $this->get('/api/good/1');
        
        // Some assertions.
    }
}

我们通过在这里将桩件 $stub 用单例模式注册给了 Container,在调用 $this->get('/api/good/1'); 时原本在 Controller 中的 GoodRepository 依赖就变成了我们自定义的桩件 $stub。我们将 $data 定义为和返回值相同的结构,注册到桩件中。这样,所有的数据都在我们可控的范围了。

如果我们在这里不使用桩件,而是直接依赖外部(数据库)中的数据,那么如果 id 为 1 的数据被删除了,我们是不是就要改成 2 了呢?我们是不是就要重新计算数据了匹配断言了呢?这样的测试,可靠性便大大降低。

后记

任何一个可靠的系统,单元测试都是必不可少的。庆幸的是,PHPUnit 帮我们提供了好用的单元测试。本文所讲的,也只是 PHPUnit 的九牛一毛。而我自己也在慢慢摸索慢慢实践中。与君共勉。

最后还是推荐去听一下 @vimac 的讲堂 PHP单元测试与测试驱动开发,受益匪浅。

查看原文

daryl 回答了问题 · 2019-02-27

使用goroutine+channel如何处理db队列

先创建几个阻塞的 channel,然后哈希一下。

关注 2 回答 1

daryl 发布了文章 · 2019-02-26

Redis 的 string

Redis 的 string

Redis 的字符串就是 SET 和 GET 操作对应的类型,算是 Redis 里最常用的类型了。

0x00 动态字符串 sds

Redis 内部的字符串表示,没有直接使用 C 语言字符串,而是对其进行了一定的改造,改造后的字符串在内存管理和长度计算方面的性能都有所提升。

举个例子,假设要存储的是字符串”redis“。

+--------+--------+-------------+
|  len   | alloc  | |r|e|d|i|s| |
+--------+--------++-----------++
                   |           |
                   v           v
                  flag        '\0'

这个图就是 sds 的内存结构。sdshdr 分四个部分,从左往右一次是字符串长度、开辟的内存空间、sdshdr 的类型以及字符串本身。在字符串初始化好之后,会返回一个指针,指向字符串本身的首地址,也就是 r 的内存地址。这样,既能方便地享受 C 语言字符串带来的兼容性,又可以对内存管理了如指掌。

0x01 redisObj

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

这是 Redis 的对象的结构,每一个 key 或者 value 都会存储成这样一个结构体。可以看一下第二个成员变量 encoding。这是 Redis 对这个对象做的编码操作。,Redis string 在存储的时候,会努力进行三种方式的编码,分别是 OBJ_ENCODING_RAWOBJ_ENCODING_INTOBJ_ENCODING_EMBSTR,针对这三种编码,尽可能地进行去优化内存空间。

最后一个成员变量则是指向具体的内容,如字符串类型的对象就直接指向对应的 sds 字符串的地址。

0x02 OBJ_ENCODING_INT

Redis 在拿到了用户 SET 命令的字符串后,如果用户 SET 了一个纯数字字符串,就会做这种优化。这种优化有一个好处,可以看到 redisObject 中的 ptr 变量,这是一个指针,也就意味着,这个变量占据 8 个字节的内存空间。8 个字节的内存空间,刚好可以存储一个 64 位的整数。Redis 会判断字符串的长度是不是小于 20,如果小于 20 则会尝试将字符串整数化。这里的 20 是因为 64 位整型可以表示的范围是 [-9223372036854775808,9223372036854775807]。

这样做了之后,就不需要额外的空间为这个“字符串”生成一个动态字符串,直接存在 ptr 就可以了。

0x03 OBJ_ENCODING_EMBSTR

Redis 在转换成整型失败后,就会尝试这种编码。这种编码其实是把 robj 和 sds 字符串放在了同一块内存空间中。这种编码对内存空间优化不大,但是它们的空间是连续的。这样做,我认为有几个好处。

  1. 开辟和回收空间的时候,只需要进行一次操作就可以了,这样做减少了 malloc 和 free 的次数。
  2. 连续的空间,对系统缓存更加友好。

目前,是对长度小于 44 的小字符串进行这种编码。

0x04 OBJ_ENCODING_RAW

在前面的几种尝试都失败之后,就只能存储成最原始的动态字符串了。但是 Redis 在这里依然还是会做一点事情,就是会把动态字符串的多余的空间给释放掉,目前(5.0)是会释放掉 10% 长度的冗余空间,如果不足 10% 就不会释放。

0x05 后记

Redis 对内存的优化真是做到了极致,这里还只是冰山一角,我了解到的还只是冰山一角。

本文为作者自己读书总结的文章,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。
查看原文

赞 5 收藏 3 评论 1

认证与成就

  • 获得 488 次点赞
  • 获得 21 枚徽章 获得 1 枚金徽章, 获得 5 枚银徽章, 获得 15 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-03-28
个人主页被 7.3k 人浏览