go get私有仓库的办法
以github.com为例,在git配置中加上:
git config --global url."git@github.com:".insteadOf "https://github.com/"
以github.com为例,在git配置中加上:
git config --global url."git@github.com:".insteadOf "https://github.com/"
golang中的glog
库是google著名开源C++日志库glog的golang版本,在golang默认日志库的基础上做了更进一层的封装使得该库能更贴近日常使用。项目首页为golang/glog,当前版本的glog有以下几个特点:
Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg
,其中L
表示日志级别首字符,如Info
日志,L
打印出来是ID`。安装glog:
go get github.com/golang/glog
glog依赖命令行参数输入,默认是打印到文件的,在使用该库前必须先使用flag
包解析命令行参数:
flag.Parse()
因为glog底层实现是先把日志输入到缓冲区buffer
中,然后定时写入文件,所以在执行完日志输出后要手动执行glog.Flush()
确保日志刷新到文件。一般在函数开始处执行:
defer glog.Flush()
输出日志时,需要加上-log_dir
参数以指定目录写入日志文件,如以下代码:
package main
import (
"flag"
"github.com/golang/glog"
)
func main() {
flag.Parse()
defer glog.Flush()
glog.Info("HelloWorld")
}
编译后执行:
./glog -log_dir=log
在log目录下会生成相应的日志文件,查看对应的日志:
打印到标准输出
默认情况下,日志不是打印到标准输出中,如需打印到标准输出可以使用以下两个参数:
logtostderr
:打印到标准错误而不是文件。alsologtostderr
:同时打印到标准错误。这两个参数都会把日志打印到标准错误中(在linux终端环境下,前台显示的标准错误和标准输出可以认为是同一个输出),调试日志时可以加上这两个参数中的任一:
v level
和v module
功能是最常用到的功能,适用于给同一套代码在不同环境下设置不同日志级别的功能。如在调试环境下打印出更多级别的日志,生产环境下打印更少的日志。
使用方法:
glog.V(n).Info("Log message")
其中n
表示的是日志级别,当n
小于等于设定日志级别时将会被打印。默认情况下日志级别时0,启动时通过-v=x
参数设定日志级别。如在代码中分别以级别3和级别5打印两条日志:
func main() {
flag.Parse()
defer glog.Flush()
glog.V(3).Info("Level 3 log")
glog.V(5).Info("Level 5 log")
}
执行时设定日志级别为4,日志将不会打印出第二条级别为5的日志:
./glog -v=4 -log_dir=log
日志:
Log file created at: 2019/09/08 18:04:55
Running on machine: maqianos
Binary: Built with gc go1.10.4 for linux/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0908 18:04:55.924127 546 main.go:12] Level 3 log
vmodule选项可以在上面设定v level之后,单独再给某个文件设置日志级别,适用于以下场景:
执行:./glog -v=4 -log_dir=log -vmodule=main=5
,日志中将会打印出级别为5的日志:
Log file created at: 2019/09/08 18:11:36
Running on machine: maqianos
Binary: Built with gc go1.10.4 for linux/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0908 18:11:36.096301 557 main.go:12] Level 3 log
I0908 18:11:36.100119 557 main.go:13] Level 5 log
默认是没有DEBUG日志的,WARN日志用得少可以把WARN日志改成DEBUG。在glog.go
源码文件中,修改以下内容:
const severityChar = "IWEF" // 改为"IDEF"
var severityName = []string{
infoLog: "INFO",
warningLog: "WARN" // 改为"DEBUG",
errorLog: "ERROR",
fatalLog: "FATAL",
}
这里实现简单,改起来也简单。
// 搜索函数init
func init() {
// 注释掉下面两行
//flag.BoolVar(&logging.toStderr, "logtostderr", false, "log to standard error instead of files")
//flag.BoolVar(&logging.alsoToStderr, "alsologtostderr", false, "log to standard error as well as files")
// 添加下面两行
logging.alsoToStderr = false
logging.toStderr = true
// Default stderrThreshold is ERROR.
logging.stderrThreshold = errorLog
logging.setVState(0, nil, false)
go logging.flushDaemon()
}
strcpy
是字符串拷贝函数,将一个字符串拷贝到另一个字符串。
代码:
char *strcpy(char *dst, const char *src) { // [1]
char *p = dst; // [2]
if (src == NULL || dst == NULL) { // [3]
return NULL;
}
while ((*dst++ = *src++) != '\0'); // [4]
return p; // [5]
}
实现该函数的注意点:
src[i] == '\0'
,'\0'也要拷贝到dst字符串中。高级:
上面的实现是没有区分内存重叠的场景,假设源目地址存在重叠,使用上面的函数实现是有问题的。如图所示:
dst和src都是指向同一个数组,当把src的第三个元素赋值到dst后,src原有的'\0'被覆盖了,后续继续拷贝就出现问题。
解决这个问题其实很简单,只要从后往前复制src即可解决这个问题。可参考下面memcpy的实现。
memcpy的作用是把一块内存区域拷贝到另外一个内存地址上面去。
实现:
void *memcpy(void *dst, const void *src, unsigned int count) { // [1]
char *pdst, *psrc;
if (dst == NULL || src == NULL || count == 0) { // [2]
return NULL;
}
if (dst == src) { // [3]
return dst;
}
if (dst > src) { // [4]
pdst = (char *)dst + count - 1;
psrc = (char *)src + count - 1;
while (count--) {
*pdst-- = *psrc--;
}
} else { // [5]
pdst = (char *)dst;
psrc = (char *)src;
while (count--) {
*pdst++ = *psrc++;
}
}
return dst; // [6]
}
需要注意的点:
memmove的作用是把一块内存空间的内容移动到另一个内存空间,实现方法和上面的memcpy一致。
vector是先行存储的,大部分时候的插入删除操作都有可能导致迭代器失效。失效场景:
示例代码:
vector<int> data;
vector<int>::iterator it;
data.push_back(100);
data.push_back(200);
data.push_back(300);
data.push_back(400);
for (it = data.begin(); it != data.end(); it++) {
cout << *it << endl;
if (*it == 300)
data.erase(it); // 删除之后将会失效
}
失效规则:
list不是线性存储的,链式存储的好处就是方便增加和删除,不会影响原有的内存空间。
失效规则:
// 首尾添加元素,在vs环境下测试失效,g++正常
void deque_f() {
deque<int> q;
deque<int>::iterator it;
q.push_back(100);
q.push_back(200);
q.push_back(300);
q.push_back(400);
for (it = q.begin(); it != q.end(); it++) {
cout << *it << endl;
if (*it == 200) // 不会失效
q.push_front(50);
if (*it == 300) // 不会失效,且最后会输出500
q.push_back(500);
}
}
map和set底层都是红黑树,不是线性存储,它们的失效规则:
class CTestA {
int m_data;
};
class CTestB : virtual public CTestA {
};
class CTestC : virtual public CTestA {
};
class CTestD : public CTestB, public CTestC {
};
内存布局:
1>class CTestD size(8):
1> +---
1> 0 | +--- (base class CTestB)
1> 0 | | +--- (base class CTestA)
1> 0 | | | m_data
1> | | +---
1> | +---
1> 4 | +--- (base class CTestC)
1> 4 | | +--- (base class CTestA)
1> 4 | | | m_data
1> | | +---
1> | +---
1> +---
CTestD的大小是8,分别包含了一份CTestA和CTestB,他们分别是4个字节,总共8字节。
class CTestA {
int m_data;
};
class CTestB : public CTestA {
int m_data_b;
};
class CTestC : public CTestA {
int m_data_c;
};
class CTestD : public CTestB, public CTestC {
int m_data_d;
};
CTestD的大小为20,其中包含CTestA的8个字节和CTestB的8个字节以及自定义的m_data_d
的四个字节:
1>class CTestD size(20):
1> +---
1> 0 | +--- (base class CTestB)
1> 0 | | +--- (base class CTestA)
1> 0 | | | m_data
1> | | +---
1> 4 | | m_data_b
1> | +---
1> 8 | +--- (base class CTestC)
1> 8 | | +--- (base class CTestA)
1> 8 | | | m_data
1> | | +---
1>12 | | m_data_c
1> | +---
1>16 | m_data_d
1> +---
多继承,如果没有使用virtual关键字,子类的大小为每个继承的父类的大小之和。其内存分布在变量的最开始部分,先排布父类的内存。
class CTestA {
int m_data;
};
class CTestB : virtual public CTestA {
int m_data_b;
};
class CTestC : public CTestA {
int m_data_c;
};
class CTestD : public CTestB, public CTestC {
int m_data_d;
};
内存排布:
1>class CTestD size(24):
1> +---
1> 0 | +--- (base class CTestB)
1> 0 | | {vbptr}
1> 4 | | m_data_b
1> | +---
1> 8 | +--- (base class CTestC)
1> 8 | | +--- (base class CTestA)
1> 8 | | | m_data
1> | | +---
1>12 | | m_data_c
1> | +---
1>16 | m_data_d
1> +---
1> +--- (virtual base CTestA)
1>20 | m_data
1> +---
1>
此时CTestD包含了以下几个部分:
CTestB
: B采用了虚继承,其父类A的m_data
不会占用D中的空间,但它内部多了一个vbptr
指针,总共占用8字节CTestC
: C没有采用虚继承,其内部的m_data
也还被D继承,没有vbptr
指针,算上C自己的m_data_c
共占用8个字节m_data_d
: D本身自己的成员变量,占用4字节vitrual base CTestA
: 虚继承于CTestA,包含CTestA的数据m_data
,占用4个字节CTestD共占用24个字节。
将CTestC也改成虚继承的方式:
class CTestC : virtual public CTestA {
int m_data_c;
};
此时CTestD的内存分布情况为:
1>class CTestD size(24):
1> +---
1> 0 | +--- (base class CTestB)
1> 0 | | {vbptr}
1> 4 | | m_data_b
1> | +---
1> 8 | +--- (base class CTestC)
1> 8 | | {vbptr}
1>12 | | m_data_c
1> | +---
1>16 | m_data_d
1> +---
1> +--- (virtual base CTestA)
1>20 | m_data
1> +---
1>
CTestD依旧占用24个字节的内存空间,不过和上面不同的是,D中属于C的8个字节空间的内容变了,其中原本属于A的m_data
内存变成了vbptr
指针。
当子类(D)的多个父类(A,B)都继承于同一个基类(A)时,且继承时都添加了virtual
关键字(属于虚继承时),子类(D)只会保存一份来自基类(A)的内存。每个父类(A,B)中会添加一个vbptr
指针指向公共的基类。
当使用虚继承时,类中会生成vbptr
指针,指向公共基类的位置。vbptr
中包含两个偏移量,第一个是vbptr
指针在当前类中的偏移量,第二个是公共的基类在当前类中的位置。例如上面的CTestD类,其内存布局为:
两个vbtr内容的解析:
CTestB.vbptr
: 第一个偏移量=CTestB.vbptr - BTestB
= 0,第二个偏移量=CTestA - CTestB
= 22。CTestC.vbptr
: 第一个偏移量=CTestC.vbptr - CTestC
= 0,第二个偏移量=CTestA - CTestC
= 16。上面的例子中,CTestB和CTestC的vbptr指针的第一个偏移量(以vbptr[0]表示)都是0,它表示的是这个偏移量对于当前类的偏移。因为类B和类C目前只有vbptr的指针,所有的类中,vbptr是在成员变量之前的,所以他们都是0。但是如果在类中加入一个虚函数,使类中产生虚指针,那么这个偏移量就不是0了。
修改类B:
class CTestB : virtual public CTestA {
int m_data_b;
public:
CTestB() {
m_data_b = 2;
}
void print_data() {
cout << m_data_b << endl;
}
virtual void hello() {
}
};
打印出C的内存分布:
1>class CTestD size(28):
1> +---
1> 0 | +--- (base class CTestB)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | m_data_b
1> | +---
1>12 | +--- (base class CTestC)
1>12 | | {vbptr}
1>16 | | m_data_c
1> | +---
1>20 | m_data_d
1> +---
1> +--- (virtual base CTestA)
1>24 | m_data
1> +---
1>
可以看到,类B中多了一个vfptr
,它指向的是虚函数表的地址,此时vbptr
放在第二位,CTestB.vbptr[1]的值就是-4。类B和类C的vbptr
的细节:
1>CTestD::$vbtable@CTestB@:
1> 0 | -4
1> 1 | 20 (CTestDd(CTestB+4)CTestA)
1>
1>CTestD::$vbtable@CTestC@:
1> 0 | 0
1> 1 | 12 (CTestDd(CTestC+0)CTestA)
B的第1个偏移变成了-4,而C的没有变化。
匿名管道是linux中的一种通信方式,为有血缘关系的进程提供数据通信。相关的api函数为:
#include <unistd.h>
int pipe(int pipefd[2]);
pipe
函数传入一个长度为2的int数组,执行成功将在内核区域分配一块内存区域,并设置pipefd为管道的文件描述符。
创建子进程后,子进程也会继承这两个描述符,对于同一个文件描述符来说,父端写,子端就可以读,字段写,父端也可以读。
工作流程可以描述为:
但是由于管道内部实现是用的环形队列,因此父子不能同时对一个管道进行读操作或者写操作,队列是单向的,两端写就有问题。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#define SEND_BUFF "HelloWorld"
#define CLOSE_FD(fd) \
do { \
if (fd != -1) { \
close(fd); \
fd = -1; \
} \
} while (0)
int main(){
int fd[2], ret, n;
char buf[1025];
ret = pipe(fd);
if (ret == -1)
goto _err;
memset(buf, 0, 1025);
pid_t pid = fork();
if (pid == -1) {
goto _err;
} else if (pid == 0) {
// 子端关闭fd1,打开fd0读
CLOSE_FD(fd[1]);
n = read(fd[0], buf, 1024);
if (-1 == n) goto _err;
printf("child read %d byte data: %s\n", n, buf);
CLOSE_FD(fd[0]);
} else {
// 父端关闭fd0,打开fd1写
CLOSE_FD(fd[0]);
n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
if (n == -1) goto _err;
printf("parent send %d bytes data: %s\n",
sizeof(SEND_BUFF), SEND_BUFF);
// 等待子进程退出
wait(0);
CLOSE_FD(fd[1]);
}
return 0;
_err:
perror("Error");
CLOSE_FD(fd[0]);
CLOSE_FD(fd[1]);
return -1;
}
管道还有一个函数pipe2
提供了更高级的选项,可以使得管道成为非阻塞读:
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);
其中,当flags置为O_NONBLOCK
时,管道为非阻塞,从非阻塞的管道读取数据时,如果此时没有数据可读,将返回-1,并设置errno为EAGAIN
。
修改子进程部分代码为:
if (pid == 0){
CLOSE_FD(fd[1]);
while (1) {
n = read(fd[0], buf, 1024);
if (-1 == n) {
if (errno == EAGAIN) {
printf("READ AGAIN\n");
// 没有数据可读时,休眠一秒,继续读
sleep(1);
continue;
}
goto _err;
}
printf("child read %d byte data: %s\n", n, buf);
break;
}
CLOSE_FD(fd[0]);
}
父进程在写数据前先睡眠5秒:
else {
CLOSE_FD(fd[0]);
sleep(5);
n = write(fd[1], SEND_BUFF, sizeof(SEND_BUFF));
// ...
}
运行结果:
fifo管道是linux文件系统中的一种文件类型,创建在磁盘上,任何进程都可以打开这个文件进行读写。
它可以解决无血缘关系进程的通信问题,创建管道的函数为:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
当管道文件被创建之后,就可像文件一样打开它进行读写:
// read.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
#include <errno.h>
#include<unistd.h>
#define FIFO_FILE "fifo"
#define BUFF_SIZE 1024
int main() {
int ret, fd = -1;
char buf[BUFF_SIZE] = { 0 };
ret = mkfifo(FIFO_FILE, 0755);
if (ret == -1 && errno != EEXIST)
goto _err;
fd = open(FIFO_FILE, O_RDONLY);
if (fd == -1)
goto _err;
bzero(buf, BUFF_SIZE);
ret = read(fd, buf, sizeof(buf) - 1);
if (ret == -1)
goto _err;
printf("read: %s\n", buf);
close(fd);
fd = -1;
return 0;
_err:
perror("Error");
if (fd > 0) {
close(fd);
fd = -1;
}
return -1;
}
写管道程序:
// write.c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#define FIFO_FILE "fifo"
int main(){
int ret, fd;
fd = open(FIFO_FILE, O_WRONLY);
if (fd == -1)
goto _err;
unlink(FIFO_FILE);
ret = write(fd, "HelloWorld", 11);
if (ret == -1)
goto _err;
close(fd);
fd = -1;
return 0;
_err:
perror("Error");
if (fd > 0) {
close(fd);
fd = -1;
}
return -1;
}
此时运行./read
,会创建管道文件,新开一个窗口就可以看到,管道的颜色和普通文件不一样,并且ls的第一个字符是p
:
默认情况下,读是阻塞的(类似于读文件),直到有数据过来为止,所以./read
执行后,会阻塞,直到运行./write
之后才会收到数据。
read: HelloWorld
非阻塞pipe有一个注意事项:当打开的管道没有其他进程也打开的话(即只有当前进程在读,没有其他进程了),直接返回0,不会返回EAGAGIN
。
// 加上O_NONBLOCK标记
fd = open(FIFO_FILE, O_RDONLY | O_NONBLOCK);
if (fd == -1)
goto _err;
bzero(buf, BUFF_SIZE);
printf("fd = %d\n", fd);
while (1) {
ret = read(fd, buf, sizeof(buf) - 1);
if (ret == -1) {
if (errno == EAGAIN) {
printf("READ AGAIN\n");
sleep(1);
continue;
}
goto _err;
} else if (ret == 0) {
printf("read end\n");
close(fd);
fd = -1;
break;
} else {
printf("read: %s\n", buf);
}
}
编译后如果直接执行,程序会直接退出,因为此刻没有其他进程在写,不会返回EAGAIN
多线程主要有以下几种同步方法:
同步方式 | 优缺点和适用范围 |
---|---|
互斥量 | 最简单的锁,使用临界区的方式锁住代码块,最基础的锁,适用于多线程互斥 |
读写锁 | 有更高的并行性,只会锁住写锁,读锁共享,适用于读远大于写的场景 |
屏障 | 适用于线程等待,直到多个线程到达同一点时再继续运行,主要用于同步 |
条件变量 | 允许线程以无竞争的方式等待特定条件的发生,适用于生产者和消费者场景 |
信号量 | 通过PV变量的方式,多用于生产者和消费者场景,和条件变量类似 |
自旋锁 | 被锁住时线程处于忙等,适用于锁被持有的时间短,不希望线程把时间花在重新调度上 |
结构体对齐是C/C++优化结构体内存排布的一种机制,它的出现是为了解决跨总线寻址的问题。
例如对于以下结构:
struct stu {
char a;
int b;
};
如果不执行结构体对齐,它在内存中的排布应该是这样的:
对于字段b而言,它占四个字节,跨了两个总线地址,如果要取出b的值就要寻址两次。而如果把结构设计成以下形式:
对b的访问就只用寻址一次了,所以,为了减少cpu消耗,编译器会对结构进行优化,会对内存进行对齐。
结构体对齐遵循的原则:
struct Stu {
char name[7];
int age;
char sex;
short id;
int friends;
};
使用vs查看内存分布可以看到,name和sex后面被填充了一个字节:
1>class Stu size(20):
1> +---
1> 0 | name
1> | <alignment member> (size=1)
1> 8 | age
1>12 | sex
1> | <alignment member> (size=1)
1>14 | id
1>16 | friends
1> +---
struct Nginx {
struct Stu stu;
char c;
};
当结构体中包含结构体时,此时最大的元素是name,它对齐后占8个字节,为了维持对齐的特性,会在Nginx成员后面继续填充:
1>class Nginx size(24):
1> +---
1> 0 | Stu stu
1>20 | c
1> | <alignment member> (size=3)
1> +---
可以看到c后面填充了八个字节。
STL中的map
和set
默认时不支持存结构体的,如果要添加结构体的支持,必须手动重载<
运算符。
原因:map和set底层都是通过红黑树实现的,红黑树搜索树的一种,插入数据时要比较大小,所以结构体必须重载小于号
示例:
#include <iostream>
#include <string>
#include <set>
using namespace std;
typedef struct stu_st {
string name;
int age;
}stu_t;
int main() {
set<stu_t> stu_infos;
stu_t a, b;
a.name = "xiaoming";
a.age = 20;
b.name = "xiaohua";
b.age = 21;
stu_infos.insert(a);
stu_infos.insert(b);
cout << stu_infos.size() << endl;
return 0;
}
以上代码在vs下编译报错:
问题很明确,没有重载<
符号,添加上以下代码即可:
bool operator<(const stu_t& a, const stu_t& b) {
return a.name < b.name;
}