struct能不能比较? 很显然这句话包含了两种情况:
在分析上面两个问题前,先跟大家梳理一下golang中,哪些数据类型是可比较的,哪些是不可比较的:
下面就跟大家分别分析一下上面两种情况吧
首先,我们构造一个struct结构体来玩玩吧
1 | type S struct { |
运行上面的代码发现会打印false。既然能正常打印输出,说明是可以个比较的,接下来让我们来个「死亡两问」
什么可以比较?
回到上面的划重点部分,在总结中我们可以知道,golang中 Slice,Map,Function 这三种数据类型是不可以直接比较的。我们再看看S结构体,该结构体并没有包含不可比较的成员变量,所以该结构体是可以直接比较的。
为什么打印输出false?
a 和 b 虽然是同一个struct 的两个实例,但是因为其中的指针变量 Address 的值不同,所以 a != b,如果a b 在初始化时把 Address 去掉(不给 Address 初始化),那么这时 a == b 为true, 因为ptr变量默认值是nil,又或者给 Address 成员变量赋上同一个指针变量的值,也是成立的。
如果给结构体S增加一个Slice类型的成员变量后又是什么情况呢?
1 | type S struct { |
这时候会打印输出什么呢?true?false?实际上运行上面的代码会报下面的错误:
1 | # command-line-arguments |
a, b 虽然是同一个struct两个赋值相同的实例,因为结构体成员变量中带有了不能比较的成员(slice),是不可以直接用 == 比较的,所以只要写 == 就报错
「总结」
同一个struct的两个实例可比较也不可比较,当结构不包含不可直接比较成员变量时可直接比较,否则不可直接比较
但在平时的实践过程中,当我们需要对含有不可直接比较的数据类型的结构体实例进行比较时,是不是就没法比较了呢?事实上并非如此,golang还是友好滴,我们可以借助 reflect.DeepEqual 函数 来对两个变量进行比较。所以上面代码我们可以这样写:
1 | type S struct { |
那么 reflect.DeepEqual 是如何对变量进行比较的呢?
DeepEqual函数用来判断两个值是否深度一致。具体比较规则如下:
「结论」:可以比较,也不可以比较
可通过强制转换来比较:
1 | type T2 struct { |
如果成员变量中含有不可比较成员变量,即使可以强制转换,也不可以比较
1 | type T2 struct { |
编译报错:
1 | # command-line-arguments |
struct必须是可比较的,才能作为key,否则编译时报错
1 | type T1 struct { |
熟悉 Golang 的朋友对于 tag、json 和 struct 都不陌生。
1 | type Address struct { |
我们可以看到,多了一行 "zip_code": "",
,而这则信息在原本的 json 数据中是没有的,但我们更希望的是,在一个地址有 zip_code 号码的时候输出,不存在 zip_code 的时候就不输出,幸运的是,我们可以在 Golang 的结构体定义中添加 omitempty
关键字,来表示这条信息如果没有提供,在序列化成 json 的时候就不要包含其默认值。稍作修改,地址结构体就变成了
1 | type Address struct { |
成功解决。
带来方便的同时,使用 omitempty
也有些小陷阱。
还是拿地址类型说事,这回我们想要往地址结构体中加一个新的结构体来表示经纬度,如果没有或缺乏相关的数据,可以忽略。新的 struct 定义如下所示
1 | type Address struct { |
读入原来的地址数据,处理后序列化输出,我们就会发现即使对新的结构体字段加上了 omitempty
关键字,输出的 json 还是带上了一个空的坐标信息。
为了达到我们想要的效果,可以把坐标结构体定义为指针类型,这样 Golang 就能知道一个指针的“空值”是多少了,否则面对一个我们自定义的结构, Golang 是猜不出我们想要的空值的。于是有了如下的结构体定义:
1 | type Address struct { |
对于用 omitempty
定义的 字段 ,如果给它赋的值恰好等于默认空值的话,在转为 json 之后也不会输出这个 字段 。比如说上面定义的经纬度坐标结构体,如果我们将经纬度两个 字段 都加上 omitempty
1 | type coordinate struct { |
这个坐标的longitude
消失不见了!
但我们的设想是,如果一个地点没有经纬度信息,则悬空,这没有问题,但对于“原点坐标”,我们在确切知道它的经纬度的情况下,(0.0, 0.0)仍然被忽略了。正确的写法也是将结构体内的定义改为指针
1 | type coordinate struct { |
这样空值就从 float64
的 0.0 变为了指针类型的 nil
,我们就能看到正确的经纬度输出。
用代码展示粘包现象
server.go
1 | package main |
client.go
1 | package main |
当client连续向server端连续发送20个数据包的时候,我们看server端打印的内容。
1 | 从client收到消息 hello, server |
按照我们预想的,server端应该受到20条消息,每一条消息只包含hello,server
才对。然而server却收到了不到20条消息,而且消息的长短不一。这就是粘包现象。
主要原因是tcp数据传递的模式是流模式,在保持长连接的时候可以进行多次收和发。
粘包可能发生在发送端也可能发生在接收端:
出现粘包的关键在于接收方不能够确定将要接收的数据包的大小,因此我们需要手动对数据进行封包和拆包操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两个部分内容了(过滤非法包时封包还会加入包尾)。包头部分的长度是固定的,并且它存储了包体的长度。根据包头的长度固定以及包头中所包含的包体的长度就能够正确的实现拆分出一个完整的数据包。
怎么去封包、解包呢?
我们可以自己定义一种协议规定,比如数据包的前几个字节为包头,里面存储的是发送的数据的长度。
proto/tcp_stick_proto.go
1 | package proto |
server.go 修改如下
1 | package main |
client.go
1 | package main |
运行,问题解决
UDP不存在粘包问题,是由于UDP发送的时候,没有经过Negal算法优化,不会将多个小包合并一次发送出去。另外,在UDP协议的接收端,采用了链式结构来记录每一个到达的UDP包,这样接收端应用程序一次recv只能从socket接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次(无论recv时指定了多大的缓冲区)
]]>题目描述:在O(nlogn)时间内对链表进行排序。
进阶:
O(nlogn)
时间复杂度和常数级空间复杂度下,对链表进行排序吗?来源:LeetCode#148
能达到此事件复杂度的排序算法如下
时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|
归并排序 | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) / 最坏O(n^2) | O(logn) / 最坏O(n) | 不稳定 |
希尔排序 | O(nlogn) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(1) | 不稳定 |
待排序的数据用链表保存,所以堆排序和希尔排序不太适合。因此这里用快速排序和归并排序实现。
快速排序速度内存情况比归并排序差很多。
思路:快慢指针交换小于哨兵的节点,快指针用于探路,慢指针用于交换数据
1 | // 时间复杂度O(nlogn) 最坏 O(n^2) 空间复杂度 O(logn) 最坏(n) |
自顶向下归并排序
对链表自顶向下归并排序的过程如下。
找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2步,慢指针每次移动 1步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。
以中间节点为界,拆分链表。然后递归地拆左右两个链表。拆到只剩一个元素的时候合并
链表合并之后,得到完整的排序后的链表。
上述过程可以通过递归实现。递归的终止条件是链表的节点个数小于或等于 1,即当链表为空或者链表只包含 1 个节点时,不需要对链表进行拆分和排序。
1 | // 时间复杂度O(nlogn) 空间复杂度 O(logn) |
使用自底向上的方法实现归并排序,则可以达到 O(1) 的空间复杂度。首先求得链表的长度 length,然后将链表拆分成子链表进行合并。
具体做法如下:
用 subLength
表示每次需要排序的子链表的长度,初始时subLength=1
。
每次将链表拆分成若干个长度为subLength
的子链表(最后一个子链表的长度可以小于subLength
),按照每两个子链表一组进行合并,合并后即可得到若干个长度为 subLength×2
的有序子链表(最后一个子链表的长度可以小于subLength×2
)。
subLength
的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于 length
,整个链表排序完毕。如何保证每次合并之后得到的子链表都是有序的呢?可以通过数学归纳法证明。
初始时subLength=1
,每个长度为 1的子链表都是有序的。
如果每个长度为subLength
的子链表已经有序,合并两个长度为subLength
的有序子链表,得到长度为subLength×2
的子链表,一定也是有序的。
当最后一个子链表的长度小于subLength
时,该子链表也是有序的,合并两个有序子链表之后得到的子链表一定也是有序的。
因此可以保证最后得到的链表是有序的。
时间复杂度O(nlogn) 空间复杂度 O(1)
1 | func sortList(head *ListNode) *ListNode { |
归并排序,更适合链表的排序,仅此而已,又水一篇
]]>在 Go 中,Slice(切片)是抽象在 Array(数组)之上的特殊类型。为了更好地了解 Slice,第一步需要先对 Array 进行理解。深刻了解 Slice 与 Array 之间的区别后,就能更好的对其底层实现更好地理解。
先通过源码查看一下go的slice底层
1 | type slice struct { |
slice
类型包括三个字段
unsafe.Pointer
可以表示任何可寻址的值的指针)关于切片底层还是移步这个视频吧,实在不想去画图长篇论述,这个up已经讲得很好了。我这篇文章只是想总结一下切片扩容。
1 | ints := []int{1, 2} // 扩容前 oldCap = 2 |
预估扩容规则
newCap
就等于所需最小容量len
。如果旧的长度小于等于1024,那么直接在旧的容量基础上翻倍扩容;如果旧的长度大于等于1024,先在旧的基础上扩1.25倍,循环往复扩充。源代码
1 | func growslice(et *_type, old slice, cap int) slice { |
所需内存 = 预估容量 * 元素类型大小
比如预估容量是3个,int类型,那么所需内存就是24,但实际分配的时候并不是直接向系统索要24个字节的内存。而是想go本身的内存管理模块申请。
Go语言的内存管理模块将内存分成了大大小小67个级别的span,其中0级代表特殊的大对象,其大小是不固定的。当具体的对象需要分配内存时,并不是直接分配span,而是分配不同级别的span中的元素。因此span的级别也不是以每个span大小为依据,而是以span中元素的大小为依据。
1 | span等级 元素大小 span大小 对象个数 |
申请内存时,内存管理模块会帮我们选足够大且最接近的内存规格,所以上面3个int类型需要24个字节的内存,那么实际分配的是第3个span等级,32个字节大小的内存span。
32个字节的内存span 能存储 大小为8个字节的int类型 共四个。所以真实的扩容容量为 4 而不是预估容量3。
扩容及内存分配部分源码
1 | func growslice(et *_type, old slice, cap int) slice { |
1、获取老 Slice 长度和计算假定扩容后的新 Slice 元素长度、容量大小以及指针地址(用于后续操作内存的一系列操作)
2、确定新 Slice 容量大于老 Sice,并且新容量内存小于指定的最大内存、没有溢出。否则抛出异常
3、若元素类型为 kindNoPointers
,也就是非指针类型。则在老 Slice 后继续扩容
capmem
,在老 Slice cap 后继续申请内存空间,其后用于扩容add(p, newlenmem)
(ptr)注:那么问题来了,为什么要重新初始化这块内存呢?这是因为 ptr 是未初始化的内存(例如:可重用的内存,一般用于新的内存分配),其可能包含 “垃圾”。因此在这里应当进行 “清理”。便于后面实际使用(扩容)
4、不满足 3 的情况下,重新申请并初始化一块内存给新 Slice 用于存储 Array
5、检测当前是否正在执行 GC,也就是当前是否启用 Write Barrier(写屏障),若启用则通过 typedmemmove
方法,利用指针运算循环拷贝。否则通过 memmove
方法采取整体拷贝的方式将 lenmem 个字节从 old.array 拷贝到 ptr,以此达到更高的效率
注:一般会在 GC 标记阶段启用 Write Barrier,并且 Write Barrier 只针对指针启用。那么在第 5 点中,你就不难理解为什么会有两种截然不同的处理方式了
这里需要注意的是,扩容时的内存管理的选择项,如下:
kindNoPointers
,将在老 Slice cap 的地址后继续申请空间用于扩容Slice 切片指向所引用的 Array。因此在 Slice 上的变更。会直接修改到原始 Array 上(两者所引用的是同一个)
随着 Slice 不断 append,内在的元素越来越多,终于触发了扩容。往 Slice append 元素时,若满足扩容策略,也就是假设插入后,原本数组的容量就超过最大值了,这时候内部就会重新申请一块内存空间,将原本的元素拷贝一份到新的内存空间上。此时其与原本的数组就没有任何关联关系了,再进行修改值也不会变动到原始数组。这是需要注意的
复制不会触发扩容,所以我们必须准备好cap够用的dst slice。
copy 函数支持在不同长度的 Slice 之间进行复制,若出现长度不一致,在复制时会按照最少的 Slice 元素个数进行复制
那么在源码中是如何完成复制这一个行为的呢?我们来一起看看源码的实现,如下:
1 | func slicecopy(to, fm slice, width uintptr) int { |
fm.array
复制 size
个字节到 to.array
的地址处(会覆盖原有的值)1 | var nums []int// nil len=0 cap=0 |
对于了解一门语言来说,会关心我们在函数调用的时候,参数到底是传的值,还是引用?其实对于传值和传引用,是一个比较常见的话题,我们必须非常清楚。对于我们做Go语言开发的来说,也必须知道到底是什么传递。
传值的意思是:函数传递的总是原来这个东西的一个副本(拷贝)。比如我们传递一个int
类型的参数,传递的其实是这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个该指针的一份拷贝,而不是这个指针。
对于int float
等这类基础类型我们可以很好的理解,它们就是一个拷贝。但是指针呢?我们觉得可以通过它修改原来的值,怎么会是一个拷贝呢?下面我们看个例子。
1 | func modify(ptr *int) { |
首先我们要知道,任何存放在内存里的东西都有自己的地址,指针也不例外,它虽然指向别的数据,但是也有存放该指针的内存。所以通过输出我们可以看到,传递指针参数是传的一个指针值的拷贝,实参形参虽然是不同的指针,但他们两个都存储了相同的地址值,即变量的i
的地址。
通过上面的图,可以更好的理解。 首先我们看到,我们声明了一个变量i
,值为10
,它的内存存放地址是0xc0000180a8
,通过这个内存地址,我们可以找到变量i
。
指针iPtr
也是一个指针类型的变量,它存放了i
的地址,这个指针本身内存地址是0xc00000e028
。 在我们传递指针变量iPtr
给modify
函数的时候,是该指针变量的拷贝,所以新拷贝的指针变量ptr
,它的内存地址已经变了,是新的0xc00000e038
。
虽然形参和实参的地址不同,但我们都可以称之为指针的指针,他们存储了同一个地址,即变量i
的地址,这也就是为什么我们可以修改变量i
的值的原因。
Go语言是没有引用传递的,这里我不能使用Go举例子,但是可以通过说明描述。
以上面的例子为例,如果在modify
函数里打印出来的形参和实参的内存地址是一样的,即&iPtr == &ptr
,那么就是引用传递。
了解清楚了传值和传引用,但是对于Map类型来说,可能觉得还是迷惑,一我们可以通过方法修改它的内容,二它没有明显的指针。
1 | func modify(m map[int]string) { |
两个内存地址是不一样的,所以这又是一个值传递(值的拷贝),那么为什么我们可以修改Map的内容呢?
先不急,我们先看一个自己实现的struct
。
1 | func modify(p Person) { |
我们发现,我们自己定义的Person
类型,在函数传参的时候也是值传递,但是它的值Name
字段并没有被修改,我们想改成李四
,发现最后的结果还是张三
。
这也就是说,map
类型和我们自己定义的struct
类型是不一样的。我们尝试把modify
函数的接收参数改为Person
的指针。
1 | type Person struct { |
我们发现,这次被修改了。我们这里内存地址的不再解释,因为我们上面int
类型的例子已经证明了指针类型的参数也是值传递的。 指针类型可以修改,非指针类型不行,那么我们可以大胆的猜测,我们使用make
函数创建的map
是不是一个指针类型呢?看一下源代码:
1 | // makemap implements Go map creation for make(map[k]v, hint). |
通过查看src/runtime/map.go
源代码发现,的确和我们猜测的一样,make
函数返回的是一个hmap
类型的指针*hmap
。也就是说map==*hmap
。 现在看func modify(p map[][])
这样的函数,其实就类似于func modify(p *hmap)
,但我们不能这样去写。这和我们前面什么是值传递里举的func modify(ip *int)
的例子一样,可以参考分析。
所以在这里,Go语言通过make
函数,字面量的包装,为我们省去了指针的操作,让我们可以更容易的使用map。这里的map
可以理解为引用类型,但是记住引用类型不是传引用。
chan
类型本质上和map
类型是一样的,这里不做过多的介绍,参考下源代码:
1 | func makechan(t *chantype, size int) *hchan { |
chan
也是一个引用类型,和map
相差无几,make
返回的是一个*hchan
。
slice
和map
、chan
都不太一样的,一样的是,它也可以在函数中修改对应的内容。
1 | func modify(nums []int) { |
运行打印结果,发现slice的确是被修改了,而且我们这里打印slice
的内存地址是可以直接通过%p
打印的,不用使用&
取地址符转换。并且地址关系是nums==&nums[0]==a== &a[0]
这就可以证明make
的slice也是一个指针了吗?不一定,也可能fmt.Printf
把slice
特殊处理了。
1 | type slice struct { |
通过查看src/runtime/slice.go
源代码发现,对于chan
、map
、slice
等被当成指针处理,通过value.Pointer()
获取对应的值的指针。
1 | // If v's Kind is Slice, the returned pointer is to the first |
很明显了,当是slice
类型的时候,返回是slice
这个结构体里,字段Data第一个元素的地址。
所以我们通过%p
打印的slice
变量的地址其实就是内部存储数组元素的地址,slice
是一种结构体+元素指针的混合类型,通过元素array
的指针,可以达到修改slice
里存储元素的目的。
所以修改类型的内容的办法有很多种,类型本身作为指针可以,类型里有指针类型的字段也可以。
单纯的从slice
这个结构体看,我们可以通过modify
修改存储元素的内容,但是永远修改不了len
和cap
,因为他们只是一个拷贝,不是指针,如果要修改,那就要传递*slice
作为参数才可以。
下面通过这个Person
和slice
对比,以便于更好理解。
1 | type Person struct { |
Person
的Name
字段就类似于slice
的len
或者cap
字段,Age
类似于slice
的array
字段。在传参为非指针类型的情况下,可以修改Age
字段,Name
字段无法被修改。这就是slice
可以修改值,而不可以更改容量和长度问题的原因所在。要修改Name
字段,就要把传参改为指针,伪代码比如:
1 | modify(&p) |
这样name
和age
字段双双都被修改了。
所以slice能够通过函数传参后,修改对应的数组值,是因为 slice 内部保存了引用数组的指针,并不是因为引用传递。
最终我们可以确认的是Go语言中所有的传参都是值传递(传值),都是一个副本。但是类型引用有引用类型,他们是:slice、map、channel。
因为拷贝的内容有时候是非引用类型(int
、float
、string
、struct
等这些),这样在函数中就无法修改原内容数据;有的是引用类型(pointer
、map
、slice
、chan
等这些),这样就可以修改原内容数据。
是否可以修改原内容数据,和传值、传引用没有必然的关系。在C++中,传引用肯定是可以修改原内容数据的,在Go语言里,虽然只有传值,但是我们也可以修改原内容数据,因为值类型的某个字段是引用类型。
这里也要记住,引用类型和传引用是两个概念。
再记住,Go里只有传值(值传递)。
]]>defer语句是Go中一个非常有用的特性,可以将一个方法延迟到包裹该方法的方法返回时执行,在实际应用中,defer语句可以充当其他语言中try…catch…的角色,也可以用来处理关闭文件句柄等收尾操作。
A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.
Go官方文档中对defer的执行时机做了阐述,分别是。
当一个方法中有多个defer时, defer会将要延迟执行的方法“压栈”,当defer被触发时,将所有“压栈”的方法“出栈”并执行。所以defer的执行顺序是LIFO的。
所以下面这段代码的输出不是1 2 3,而是3 2 1。
1 | func stackingDefers() { |
先看下面两个方法执行的结果。
1 | // 第一种: |
上面的方法会输出0,下面的方法输出1。上面的方法使用了匿名返回值,下面的使用了命名返回值,除此之外其他的逻辑均相同,为什么输出的结果会有区别呢?
要搞清这个问题首先需要了解defer的执行逻辑,文档中说defer语句在方法返回“时”触发,也就是说return和defer是“同时”执行的。
第一种:以匿名返回值方法举例,过程如下。
retValue
,相当于执行retValue = result
)在这种情况下,defer中的修改是对result
执行的,而不是retValue
,所以defer之后返回的依然是retValue
第二种:以命名返回值方法举例,
retValue
的过程,result
就是retValue
,defer对于result
的修改也会被直接返回。看下面的代码
1 | func deferInLoops() { |
defer在紧邻创建资源的语句后生命力,看上去逻辑没有什么问题。但是和直接调用相比,defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。所以在循环中定义defer可能导致大量的资源开销,在本例中,可以将f.Close()
语句前的defer去掉,来减少大量defer导致的额外资源消耗。
一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。
正确写法如下。
1 | resp, err := http.Get(url) |
当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()
方法退出程序时,defer并不会被执行。
1 | func deferExit() { |
上面的defer并不会输出。
]]>在大多数语言中,字符串是可以通过下标访问的,但是在go语言中,有些时候却不能做到这样。
1 | func main() { |
在解决这个问题之前,要先了解一下数组:数组是用于存储多个相同类型数据的集合。并且数组在申请内存的时候,是一次申请一块连续的内存。比如我们创建一个数组,里面存了这几个元素。
由于内存是连续的,元素的类型也是相同的,每一个元素所占用的储存空间也是固定的,比如Java中的char类型占用两个字节。数组的内存空间是平等划分的。
在可以用下标访问的语言中,字符串都是按照字符编码的。也就是将字符串"abcd"
赋给变量 a,本质上是创建了一个字符数组char[]
用来存放字符串。每一个字符占用的空间相同。
但是go语言中,字符串是按照字节编码的。 26 个英文字母,数字等一些字符,在 go 语言的 string 里面就占用一个字节。而对于中文日文韩文就不一样了, go 语言内建只支持 utf8 编码,在 utf8 里面,有一部分汉字占用 3 个字节,一部分汉字占用 4 个字节。比如 "1我"
这个字符串,打印一下它的长度,发现打印了4。这是 string 占用 4 个字节,字符”1”占用一个字节,加上”我”之后占用 4 个字节,数字占用一个字节,我占用3个字节。这样应该能理解按字节编码的意思了。
1 | func main() { |
为什么go语言要采用字节来编码呢?是为了节省空间。在utf8编码中,一些中文字符占用3个字节,有一些要占用4个字节,而英文字母只需要占用1个字节。如果采用按照字符编码的形式,一个中文算一个字符,一个英文字母也算一个字符,但是占用的内存相差很大,假设有一个超长字符串,里面有英文字符远多于中文字符,如果按字符来存储,每个字符要分配四个字节。每个字符分配四个字节是因为低于四个字节,有可能有些中文就不能正常存储了,在这种情况下,每存储一个英文字母,就要浪费三个字节的内存空间。
1 | func main() { |
go语言提供了另一种方式rune
类型来实现游标访问字符串
golang中还有一个byte数据类型与rune相似,它们都是用来表示字符类型的变量类型。它们的不同在于:
1 | // byte is an alias for uint8 and is equivalent to uint8 in all ways. It is |
1 | func main() { |
Unicode 与 ASCII 类似,都是一种字符集。
字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有一个唯一的 ID,例如上面例子中的 a 在 Unicode 与 ASCII 中的编码都是 97。汉字“你”在 Unicode 中的编码为 20320,在不同国家的字符集中,字符所对应的 ID 也会不同。而无论任何情况下,Unicode 中的字符的 ID 都是不会变化的。
UTF-8 是编码规则,将 Unicode 中字符的 ID 以某种方式进行编码,UTF-8 的是一种变长编码规则,从 1 到 4 个字节不等。编码规则如下:
1 | Unicode符号范围 | UTF-8编码方式 |
跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0
,则这个字节单独就是一个字符;如果第一位是1
,则连续有多少个1
,就表示当前字符占用多少个字节。
下面,还是以汉字严
为例,演示如何实现 UTF-8 编码。
严
的 Unicode 是4E25
(100111000100101
),根据上表,可以发现4E25
处在第三行的范围内(0000 0800 - 0000 FFFF
),因此严
的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx
。然后,从严
的最后一个二进制位开始,依次从后向前填入格式中的x
,多出的位补0
。这样就得到了,严
的 UTF-8 编码是11100100 10111000 10100101
,转换成十六进制就是E4B8A5
。 (转自阮一峰 - 字符编码笔记:ASCII,Unicode 和 UTF-8)
根据这个规则,拉丁文语系的字符编码一般情况下每个字符占用一个字节,而中文每个字符占用 3 个字节。
广义的 Unicode 指的是一个标准,它定义了字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。
在c语言中,字符串以一个\0
结束一个字符串。而在go语言中不是这样的。
上源码
1 | type stringStruct struct { |
如上就是go语言的string结构体,string类型是一个不可变类型,那么任何对string的修改都会新生成一个string的实例,如果是考虑效率的场景就要好好考虑一下如何修改了。
只能用下标访问一些特殊的字符;不能直接修改字符串;字符串转字节切片会重新分配一块内存。
1 | s1 := "111我" |
推荐文章
]]>总结在学习Go语言中集合元素-哈希,也就是Map的实现原理过程中的知识点(一)。
哈希表是除数组之外,最常见的数据结构,几乎所有的语言都会有数组和哈希表两种集合元素有的语言将数组实现成列表,有的语言将哈希表称作结构体或者字典,但是它们是两种设计集合元素的思路,数组用于表示元素的序列,而哈希表示的是键值对之间映射关系,只是不同语言的叫法和实现稍微有些不同。
哈希表1是一种古老的数据结构,在 1953 年就有人使用拉链法实现了哈希表,它能够根据键(Key)直接访问内存中的存储位置,也就是说我们能够直接通过键找到该键对应的一个值。它之所以重要不仅仅是因为它O(1)
的读写删除性能优秀,还因为他提供了键值之间的映射。想要实现一个性能优异的哈希表就必须要具备两个因素——完美的哈希函数和冲突解决方法。
从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。开放定址法需要的表长度要大于等于所需要存放的元素。在开放定址法中解决冲突的方法有:线行探查法、平方探查法、双散列函数探查法。
线行探查法是开放定址法中最简单的冲突处理方法,它从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。复杂度最坏O(n)
平方探查法即是发生冲突时,用发生冲突的单元d[i], 加上 1²、 2²等。即d[i] + 1²,d[i] + 2², d[i] + 3²…直到找到空闲单元。在实际操作中,平方探查法不能探查到全部剩余的单元。不过在实际应用中,能探查到一半单元也就可以了。若探查到一半单元仍找不到一个空闲单元,表明此散列表太满,应该重新建立。
这种方法使用两个散列函数hl和h2。其中hl和前面的h一样,以关键字为自变量,产生一个0至m—l之间的数作为散列地址;h2也以关键字为自变量,产生一个l至m—1之间的、并和m互素的数(即m不能被该数整除)作为探查序列的地址增量(即步长),探查序列的步长值是固定值l;对于平方探查法,探查序列的步长值是探查次数i的两倍减l;对于双散列函数探查法,其探查序列的步长值是同一关键字的另一散列函数的值。
如文章1中介绍
文章1中和上面 介绍了 开放定址法 和 拉链法,下面来说一下它们之间的优点和缺点
拉链法优点:
①拉链法处理冲突简单,且无堆积现象,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点而将空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
拉链法缺点和开放定址法优点:
①在拉链法中,指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
开放定址法缺点:
①开放定址法的缺点在于删除元素的时候不能真的删除,否则会引起查找错误,只能做一个特殊标记。只到有下个元素插入才能真正删除该元素。
拉链法是哈希表中最常见的实现方法,大多数的编程语言都用拉链法实现哈希表。例如在jdk1.8之后采用 链表+红黑树 数据结构来去存取发生哈希冲突的输入域的关键字。
再哈希法
就是同时构造多个不同的哈希函数: Hi = RHi(key) i= 1,2,3 ... k;
当H1 = RH1(key)
发生冲突时,再用H2 = RH2(key)
进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。
建立公共溢出区
将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。
]]>让Golang中并发控制像Python中那样优雅
先来看一下Python中的优雅实现方式
1 | from concurrent.futures import ThreadPoolExecutor |
在ThreadPoolExecutor
中直接可以确定线程的数量,是不是很优雅。在go语言中如何实现如此优雅的协程控制呢。
使用go自带的WaitGroup无法控制最多使用多少个协程,如下的代码对每个URL请求,都要进行add操作,无法对协程的数量进行真正的控制。
1 | func main() { |
通过channel可以简单地实现这个需求,上代码
1 | package gopool |
Go 语言中反射的操作主要定义在标准库 reflect
中,在标准库中定义了两种类型来表现运行时的对象信息,分别是:reflect.Value
(反射对象的值)和 reflect.Type
(反射对象的类型),Go 语言中所有反射操作都是基于这两个类型进行的。
用到的结构体
1 | type User struct { |
既然 Go 语言中所有反射操作都是基于 Value
和 Type
进行的,那么想要进行反射操作,首先就要获取到反射对象的这两个类型对象才可以。
reflect
包提供了两个函数:reflect.ValueOf(x)
和 reflect.TypeOf(x)
,通过这两个函数就可以方便的获取到任意类型(用 interface{}
表示任意类型)的 Value
对象和 Type
对象。
1 | u := User{"tom", 27, "beijing"} |
知道 Value
对象后,也可以通过 Value.Type()
方法获取到 Type
对象。例如:
1 | t1 := v.Type() |
可以看到输出结果为 true
。
通过 Type
类型对象也可以获取到 Value
类型对象,不过是零值的指针。例如:
1 | v1 := reflect.New(t) |
结果为:&{ 0}
Kind
表示反射对象的类型 Type
所代表的具体类型,零值表示无效的类型,具体有以下类型值:
1 | type Kind uint |
可以通过 Value.Kind()
或者 Type.Kind()
函数获得。例如:
1 | // 获取 Kind 类型 |
可以看到两种方式获取的结果是一样的,都是 struct
。
反射能够操作的字段和方法必须是可导出(首字母大写)的。
反射对象的字段值修改要通过调用 Value
类型的方法 Elem()
后返回的 Value
对象值来操作。
reflect.ValueOf(x)
只有当X是指针的时候,才可以修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。
Elem()
方法定义:func (v Value) Elem() Value
,返回 v
包含的值或指针 v
指向的值,如果v
的 Kind
不是 Interface
或 Ptr
,则会 panic。reflect.Indirect()
如果参数是指针的 Value
,则相当于调用了 Elem()
方法返回的值,否则返回 Value
自身值。对上面方法的一种优化操作。1 | func Indirect(v Value) Value { |
示例:反射修改综合应用
1 | // 修改反射对象的值 |
1 | // 反射字段操作 |
可以看到 age字段被修改了
可以通过 Value
的 Method()
方法或 Type
的 Method()
方法,两种形式获取对象方法信息进行反射调用,略有不同,示例如下:
示例一:简单调用
1 | u := User{"tom", 27, "beijing"} |
示例二:传参调用
1 | // 反射方法操作 |
注意:上面的方式只能反射到了值接收器的方法,对于指针接收器的方法,需要采用如下的方式
1 | u := User{"tom", 27, "beijing"} |
可以看到,值类型T
只有值接收器方法,而指针类型*T
同时有值接收器方法和指针接收器方法。
所以总结一下:
通过 reflect.ValueOf()
可以把任意类型对象转换为 Value
类型对象,也可以通过 Value
类型的方法 Interface()
把 Value
类型对象还原为原始数据类型对象。
当执行reflect.ValueOf(interface)
之后,就得到了一个类型为relfect.Value
变量,可以通过它本身的Interface()
方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。
已知原有类型
1 | // Value 转原始类型 |
未知原有类型
1 | // 通过接口来获取任意参数,然后一一揭晓 |
如何写出扩展性好的代码?这是我工作最近半年来一直在考虑的问题。不管自己做一套系统还是接手别人的项目,只要你的项目需要和别人交互,这个问题都是需要考虑的。我们今天只说说如何写出扩展性好的函数代码。代码都以golang示例。
原文:https://zhuanlan.zhihu.com/p/166698242
函数声明首先是函数名字要具有自解释性,这个要说到代码注释了,这里就不赘述了。除了函数声明外,还有函数的形参定义。这里以一个例子来说一下扩展性好的函数的参数应该如何定义。
假设我们需要一个简单的server,我们可以像下面这样定义,addr表示server启动在哪个端口上。
1 | func NewServer(addr string) |
第一期的需求很简单,就上面这些足够满足了。项目上线跑了一段时间发现,由于连接没有设置超时,很多连接一直得不到释放(异常情况),严重影响服务器性能。好,那第二期我们加个timeout。
1 | func NewServer(addr string, timeout time.Duration) |
这个时候尴尬的情况出现了,调用你代码的所有人都需要改动代码。而且这只是一个改动,之后如果要支持tls,那么又得改动一次。
解决上面的窘境的一种方法是使用不定参数。下面先简单介绍一下不定参数 , golang也是支持不定参数的,比如我要实现一个整数加法。
1 | func Add(list ...int) int { |
上面是所有的变参都是同一种类型,如果是不同的类型可以使用interface,使用反射来判断其类型。
1 | func Varargs(list ...interface{}) { |
但是如果是我们自己定义的函数的话,类型通常是知道的,也就不需要上面那么麻烦地再去判断一次,可以直接进行类型转换。
1 | func Varargs(list ...interface{}) { |
但是这么做比较危险,使用的时候必须严格按照说明进行传参,任何一种类型不正确,程序将panic。
还有一个问题就是不定参数不能为空,或者说传入的实参必须是形参的一个严格前缀。
相比于上面两种方法更好一点的是把所有参数封装成struct,这样函数声明看起来很简单。
1 | type Param struct { |
封装成struct的方式应该是一种对参数比较好的组织形式,之后函数不管怎么扩张,
只需要增加struct成员就好,而不需要改变函数声明了。而struct的坏处在什么地方呢?比如上面的Param.x是int型,如果我们不设置x,也就是下面这样传参.
1 | p := &Param{ |
这个时候Varargs看到的Param.x的0。你让Varargs怎么想?用户没有设置x(忘记设置?想使用默认值?)?用户把x设置成0?这真的有点尴尬。但是这个问题还是有解决方案的?1.避开默认值,int型不使用0,string类型不使用””。2.使用指针,用户没有设置的时候x==nil,设置的时候对x解引用(*x)取得值。这两种方式不管怎么来看,都是十分的反人类,一点也不好。
option的方式的最早是由
提出,Rob Pike就不做介绍了,感兴趣的可以看他的wiki连接。我们把option参数封装成一个函数传给我们的目标函数,所有相关的工作由函数来做。举个栗子,我们现在要写个Server,timeout和tls都是可选项,那么可以像下面这么来写(所有error handle都省去)。
1 | func NewServer(addr string, options ...func(*Server)) (*Server, error) { |
这么写的好处一目了然,横向扩展起来特别方便,而且解决上面的提到的基本所有的问题。
正常单一功能的函数实现没有什么好说的。如果需要根据不同的条件来执行不同的行为的话,这个应该怎么做的?举个例子,我现在在公司做一个优惠券的项目,用户领券和使用券的时候有一些规则,比如每人每日限领3张等。这些规则肯定不会一成不变,也许第一期是2个规则,第二期就变成4个规则了。正常可能会像下面这么写。
1 | func ruleVerify() { |
或者用switch-case。虽然很多人说switch-case写起来要比if-else更好看或者高端一点,其实我并不这么觉得。if-else和switch-case本质上并没有什么区别,扩展的时候如果需要多加一个条件分支,这两种方法改动起来都比较丑。下面说说我的解决方案。
熟悉设计模式的肯定对工厂模式肯定不会陌生。工厂模式的意思是通过参数来决定生成什么样的对象实例。我这里并不是说直接使用工厂模式而是使用工厂模式这种思想来编程。举个典型的例子,webserver的router实现方式:根据不同的路由(/foo,/bar)对应到不同的handler。光这么说,可能很多人还是不明白这种方式的扩展性好在什么地方。下面从0到1来感受一下。
首先根据不同的条件对应不同的handler,这个最简单的是使用Map来实现,没有问题,但是map里面存什么呢?如果我要增加一个条件以及对应的处理函数的时候怎么做呢?
1 | //存放 <cond, handler> 对应关系 |
代码主要分三个部分:1.mux用来存放cond和handler的对应关系;2.register用来注册新的handler; 3.提供给外部的代码入口。下面到了最核心的问题了,如果某一天PM和你说:大神,我们现在要新加一个用户用券规则。这个时候你就可以和她说:没问题。代码上的改动只需要实现一个新增规则的实现函数,同时调用一下register即可。
我经常在想什么样的代码才是好的代码?我相信每个人都会有不同的答案。从我个人角度来看,扩展性确实是衡量好代码的一个很重要的指标。在做业务系统的时候经常为了赶进度写的代码而忽略扩展性,最后随着版本迭代发现之前的代码框架越来越臃肿,不得不进行重构。重构,从某种意义上来说前面赶进度少花的时间最后都会花几倍的时间.
]]>golang的语言中提供了断言的功能。golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型,那么转回原有的类型就要用到断言了。
如以下的代码:
1 | func funcName(a interface{}) string { |
编译器将会返回:
cannot convert a (type interface{}) to type string: need type assertion
此时,意味着整个转化的过程需要类型断言。类型断言有以下几种形式:
1)直接断言使用
和类型转换不同,类型断言是将接口类型的值x,转换成类型T。类型断言的必要条件是x是接口类型,非接口类型的x不能做类型断言:
1 | value, ok := x.(T) |
但是如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断
value, ok := a.(string)
如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时value将会得到所期待的正确的值。示例:
1 | value, ok := a.(string) |
2)另外也可以配合switch语句进行判断:
type switch语句中可以有一个简写的变量声明,这种情况下,等价于这个变量声明在每个case clause隐式代码块的开始位置。如果case clause只列出了一个类型,则变量的类型就是这个类型,否则就是原始值的类型。
1 | var t interface{} |
Go语言导包的几种方式
1 | import "./module" //当前文件同一目录的module目录,此方式没什么用并且容易出错 |
1 | import "LearnGo/init" //加载gopath/src/LearnGo/init模块 |
下面几种特殊的操作
1 | import . "fmt" |
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")
可以省略的写成Println("hello world")
1 | import f "fmt" |
别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world")。
1 | import _ "fmt" |
_ 操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数,要理解这个问题,需要看下面这个图,理解包是怎么按照顺序加载的。
程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它 只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先 将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开 始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。此外需了解别名操作方式导入包也会执行init函数。
众所周知,反射是框架设计的灵魂。反射在很多语言中都有其妙用。在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。本文将对于Golang的反射的笔记。
Golang提供了一种机制,在编译时不知道类型的情况下,可更新变量、运行时查看值、调用方法以及直接对他们的布局进行操作的机制,称为反射。
目的就是增加程序的灵活性,避免将程序写死在代码里。借助反射透视一个未知的类型。
reflect提供了两种类型来进行访问接口变量的内容
First Header | Second Header |
---|---|
reflect.ValueOf() | 获取输入参数接口中的数据的值,如果为空则返回0 <- 注意是0 |
reflect.TypeOf() | 动态获取输入参数接口中的值的类型,如果为空则返回nil <- 注意是nil |
1 | func main() { |
1 | type User struct { |
输出:
1 | 这个类型的名称是: User |
1 | type User struct { |
输出:
1 | reflect.StructField{Name:"Student", PkgPath:"", Type:(*reflect.rtype)(0x10bb640), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:true} |
1 | type Student struct { |
1 | type User struct { |
输出:
1 | main.User{Student:main.Student{Id:1, Name:"反射"}} |
1 | type Student struct { |
输出:
1 | hello, 反射 |
上述详细说明了Golang的反射reflect的各种功能和用法,都附带有相应的示例,相信能够在工程应用中进行相应实践,总结一下就是:
Golang的反射很慢,这个和它的API设计有关。Golang reflect慢主要有两个原因
Go 不支持继承,但它支持组合(Composition)。组合一般定义为“合并在一起”,即将几个结构体嵌套起来构成大的结构体类型。汽车就是一个关于组合的例子:一辆汽车由车轮、引擎和其他各种部件组合在一起。
组合的典型例子就是博客博文。每一个博客的博文都有标题、内容、发表时间和作者信息。使用组合可以很好地表示它们。网站结构体中 放有 文章结构体,文章结构体中 放有 作者信息结构体。下面用代码实现。
首先定义一个作者信息author结构体和一个获取作者信息的方法fullName()
方法,其中 author
作为接收者类型,该方法返回了作者的全名。
1 | type author struct { |
下面定义一个 博文信息post结构体,以及他的一个方法。它有一个嵌套的匿名字段 author
。该字段指定 author
组成了 post
结构体。现在 post
可以访问 author
结构体的所有字段和方法。我们同样给 post
结构体添加了 details()
方法,用于打印标题、内容和作者的信息。
关键知识点: 一旦结构体A内嵌套了一个结构体B,Go 可以使我们访问嵌套的B的所有字段和方法,好像这些字段属于外部结构体一样。所以下面的 p.author.fullName()
可以替换为 p.fullName()
。
1 | type post struct { |
整合运行结果
1 | package main |
1 | title: 1 |
结合上面代码,我们加入一个website结构体,一个website结构体中包含多个post结构体,所以采用一个切片来存储。
注意:结构体不能嵌套一个匿名切片。我们需要一个字段名。所以我们来修复这个错误,让编译器顺利通过。
1 | package main |
1 | view website |
在主函数中,我们创建了一个作者 author1
,以及两篇博文 post1
、post2
。我们最后通过嵌套两篇博文构成的切片,创建了网站 w
,并在下一行调用website结构体的 view方法显示内容。
2019这一年,对我来说,可以说是变化非常大的一年。从刚刚进入大学,只会写c语言在oj刷题的渣渣,到今天可以独立完成网站前后端设计开发。这一路接受了很多了人的帮助,在此表示感谢,感谢付出和谆谆教导。
学习前端开发。从HTML、css到JavaScript。进行了系统的学习,包括css选择器,权重,各种布局,JavaScript闭包,预编译,继承, 作用链等,这些知识虽然学了也没咋用到过。更更不幸的是,其中很多的知识大部分遗忘。唉,太难了。
转向学习Java后端开发,对JavaSE基础,注解、泛型、反射 以及MySQL数据库、servlet、cookie、session、等进行了系统学习。学完后,完成了一个小小的人员管理系统,跟着视频做的。之后顺利的通过选拔进入了学院的软件实验室,这也是我的转折点。
进入实验室后,和学长组队参见了一个软件比赛,在做比赛项目的过程中学习了spring框架 、mybatis、springmvc等框架。不过仅限于会用,而且不太熟练。暑假留校,独立完成了基于人脸识别的宿舍考勤app的前后端设计与开发。app用html5写的,后端采用的ssm架构,结合json通过ajax通信。
虽然学了很多的框架和技术,但这也只是皮毛。我意识到自己的很多的不足之处,其中最为致命的是基础知识不够扎实,遇到不确定的代码就得上百度搜索一番,唉,太难了。之后我边学框架,边补自己的不足之处。还有最重要的就是学习数据结构和算法了,因为这学期开数据结构了。这门课挺重要的。
现在手头上有一个给学校开发的辅导员在线考试系统。用vue + axios + springBoot + mysql + redis+ mybatis 开发的前后端分离的项目。正在开发中,项目不是很大。但总能得到一些锻炼。
最近很忙,因为有数据结构与算法实训。实训主要内容是 用c语言写了几个小项目,班级学生管理系统、电子词典和车站车次管理系统等,上手不是很轻松,尤其是多维指针那一块。我又又又意识到自己真的基础很不扎实。
总的来说吧,这一年来进步还是挺大的,还算满意。但同时也越来越觉得自己的基础不是很好,虽然学的东西不少,但都不扎实。接下来要做的就是 好好学习专业课,补自己的短板。还有下学期专业课挺多的,操作系统和计算机组成原理,不知道难度如何。
吧啦吧啦,就这些吧。2019年过去了,有进步也有不足,所以2020年继续加油吧。争取在大学三下考研 或者 拿到一线大厂offer。
]]>少抱怨,多思考,明天会更美好。
以前自己的博客站点是托管在Github的,使用过Netlify,coding,但速度都不是很理想。也曾使用过Gitee(速度虽然不从,但绑定域名要付费99, 放弃 )。最近买了阿里云的学生机来玩,国内速度还不错,1核2G5Mbps。所以今天计划把自己原来托管在github的网站迁移到阿里云服务器。迁移过程还算顺利。
我迁移大体的思路是,继续用hexo部署。搭建自己的git服务器,然后用hexo部署到自己的git服务器。然后通过nginx挂载git仓库,实现全静态访问。速度可想而知。
开始操作
1 | $ yum install -y git |
先创建一个git
用户,用来运行git
服务:
1 | $ adduser git # 添加git用户 用于ssh |
home/git
的目录下,创建一个名为hexoBlog
的裸仓库(bare repo)。1 | $ cd /home/git/ |
上面已经创建的裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git
结尾。然后,把owner改为git
用户
1 | $ sudo chown -R git:git /home/git/hexoBlog.git |
禁用shell登录:
出于安全考虑,第二步创建的git用户不允许登录shell,这可以通过编辑/etc/passwd
文件完成。找到类似下面的一行:
1 | git:x:1001:1001:,,,:/home/git:/bin/bash |
改为:
1 | git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell |
克隆远程仓库:
现在,可以通过git clone
命令克隆远程仓库了,在自己的电脑上运行:
1 | $ git clone git@server:/home/git/hexoBlog.git # server 为ip |
出现上面信息,基本说明搭建完毕,剩下的推送就简单了。
创建hooks钩子的目的是把,git仓库里的代码拉到一个路径下,便于查看修改和以后的nginx挂载
在hooks下创建post-receive脚本,它将在仓库接收到push时执行。
1 | $ vim /home/git/hexoBlog.git/hooks/post-receive |
写到这里,用户组对/home/hexoBlog路径只有读的权限,没有写的权限。上边的配置都没有什么问题,就这个权限折腾了一天,用户组默认的权限是没有写权限的,配置好不能上传代码,问题就在用户组的权限。
修改目录及其子文件的权限
1 | $ chmod -R 777 /home/hexoBlog # 让所有用户有操作权限 |
之后push之后就可以在 /home/hexoBlog看到push的文件了
安装 启动 测试Nginx
1 | $ yum install -y nginx |
能够正常获取以下欢迎页面说明Nginx安装成功。
1 | Connecting to 127.0.0.1:80... connected. |
查看 Nginx 的默认配置的安装位置
nginx -t
修改Nginx的默认配置,其中 vim 后边就是刚才查到的安装位置(每个人可能都不一样)
1 | $ vim /etc/nginx/nginx.conf |
重启 Nginx 服务
1 | $ systemctl restart nginx |
至此,服务器端配置就结束了。接下来,就剩下本地 hexo 的配置更改了。
打开你本地的 hexo 博客所在文件,打开站点配置文件(不是主题配置文件),做以下修改。
1 | deploy: |
在 hexo 目录下执行部署,试试看。
1 | $ cd 你的hexo目录 |
打开你的公网 IP,看是不是已经部署成功了。
平衡二叉树(AVL),是一种二叉排序树,其中每个结点的左子树和右子树的高度差至多等于1。它是一种高度平衡的二叉排序树。高度平衡?意思是说,要么它是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。
平衡因子BF:
指的是二叉树左子树深度减去右子树深度。
平衡因子只能是-1,0,1。只要二叉树有一个节点的平衡因子绝对值大于1,则该树就不平衡了。
一棵AVL树有如下必要条件:
AVL平衡二叉树的查找、插入、删除操作在平均和最坏的情况下都是O(logn),这得益于它时刻维护着二叉树的平衡。如果我们需要查找的集合本身没有顺序,在频繁查找的同时也经常的插入和删除,AVL树是不错的选择。不平衡的二叉查找树在查找时的效率是很低的,因此,AVL如何维护二叉树的平衡是我们的学习重点。
由于在A的左孩子(L)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1增至2。下面图1是LL型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B顺时针旋转一样。
LL型调整的一般形式如下图所示,表示在A的左孩子B的左子树BL(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将A的左孩子B提升为新的根结点;②将原来的根结点A降为B的右孩子;③各子树按大小关系连接(BL和AR不变,BR调整为A的左子树)。
和LL型旋转类似
表示在A的右孩子B的右子树BR(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将A的右孩子B提升为新的根结点;②将原来的根结点A降为B的左孩子;③各子树按大小关系连接(AL和BR不变,BL调整为A的右子树)。
表示在A的左孩子B的右子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将B的右孩子C提升为新的根结点;②将原来的根结点A降为C的右孩子,B变为C的左孩子;③各子树按大小关系连接(BL和AR不变,CL和CR分别调整为B的右子树和A的左子树)。
等价于 先一次RR旋转再一次LL旋转。先对B右孩子C及C的右孩子进行RR旋转,再对A,B,C进行LL旋转。
表示在A的右孩子B的左子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将B的左孩子C提升为新的根结点;②将原来的根结点A降为C的左孩子,B变为C的右孩子;③各子树按大小关系连接(AL和BR不变,CL和CR分别调整为A的右子树和B的左子树)。
等价于 先一次LL旋转再一次RR旋转。先对B左孩子C及C的左孩子进行LL旋转,再对A,B,C进行RR旋转。
1 | // E [3374] - 数据结构实验之查找二:平衡二叉树 |
b站视频连接:
https://www.bilibili.com/video/av37955102?from=search&seid=14638889623357631324
https://www.bilibili.com/video/av37955178?from=search&seid=14638889623357631324
https://www.bilibili.com/video/av37955231?from=search&seid=14638889623357631324
一、基本数据类型 用(==) 进行比较的时候,比较的实际值
二、包装数据类型 用(==)进行比较的时候,比较的是在内存中存放的地址
三、包装类型中的equals方法,(String,Integer,Date)等重写了equals方法,比较的是地址和内容,地址相同返回true,地址不同但值相同返回true,其他返回false。没有重写equals方法的,比较的还是内存地址。
四、StringBuffer 和 StringBuilder 比较特殊, == 和 equals都是比较的地址。
推荐文章
]]>