TigerBeetle의 코딩 스타일 원칙과 안전성, 성능, 개발자 경험에 대한 설계 목표를 설명합니다.
“극도로 어려운 것은 세 가지가 있다. 강철, 다이아몬드, 그리고 자기 자신을 아는 것이다.” — Benjamin Franklin
TigerBeetle의 코딩 스타일은 진화하고 있습니다. 공학과 예술의 교차점에서 이루어지는 집단적인 주고받음입니다. 숫자와 인간의 직관. 이성과 경험. 제1원리와 지식. 정밀함과 시. 꼭 음악과도 같습니다. 타이트한 비트. 희귀한 그루브. 운율을 맞추는 단어들과 운율을 깨는 운율. 바이오디지털 재즈. 이것이 우리가 그 과정에서 배운 것입니다. 최고의 것은 아직 오지 않았습니다.
스타일을 다른 말로 하면 설계입니다.
“디자인은 단지 그것이 어떻게 보이고 느껴지는가가 아니다. 디자인은 그것이 어떻게 작동하는가이다.” — Steve Jobs
우리의 설계 목표는 안전성, 성능, 그리고 개발자 경험입니다. 이 순서대로입니다. 셋 다 중요합니다. 좋은 스타일은 이 목표들을 진전시킵니다. 이 코드가 안전성, 성능, 또는 개발자 경험을 더 좋게 만드는가, 아니면 더 나쁘게 만드는가? 이것이 우리가 스타일을 필요로 하는 이유입니다.
이렇게 말하면, 스타일은 단순한 가독성보다 더 큰 것이고, 가독성은 기본 입장료이며, 그 자체가 목적이 아니라 목적을 위한 수단입니다.
“...프로그래밍에서 스타일은 직접 추구할 대상이 아니다. 스타일은 이해가 부족한 곳에서만 필요하다.” ─ Let Over Lambda
이 문서는 우리가 이러한 설계 목표를 코딩 스타일에 어떻게 적용하는지 탐구합니다. 먼저 단순함, 우아함, 그리고 기술 부채에 대해 한마디 하겠습니다.
단순함은 만능 면죄부가 아닙니다. 그것은 우리의 설계 목표와 충돌하지 않습니다. 양보나 타협일 필요도 없습니다.
오히려 단순함은 우리가 설계 목표들을 함께 엮어내는 방식이며, 축들을 동시에 해결하는 “초핵심 아이디어”를 식별하여 우아한 무언가를 이루는 방식입니다.
“단순함과 우아함은 그것을 이루기 위해 힘든 노력과 규율이 필요하기 때문에 인기가 없다” — Edsger Dijkstra
대중적 믿음과는 달리, 단순함은 첫 시도가 아니라 가장 어려운 수정이기도 합니다. “단순하게 해봅시다”라고 말하는 것은 쉽지만, 실제로 그렇게 하려면 생각과 여러 번의 반복, 많은 스케치가 필요하며, 그래도 “하나를 버려야” 할 수 있습니다.
그렇다면 가장 어려운 부분은 모든 것에 얼마나 많은 생각이 들어가느냐입니다.
우리는 이 정신적 에너지를 나중이 아니라 미리, 반응적으로가 아니라 선제적으로 씁니다. 생각이 끝나면 설계에 들인 비용은 구현과 테스트 비용에 비해 작아지고, 운영과 유지보수 비용에 비하면 더더욱 작아진다는 것을 알기 때문입니다.
설계에 들인 한 시간이나 하루는 프로덕션의 몇 주, 몇 달의 가치가 있습니다.
“단순하고 우아한 시스템은 설계와 올바른 구현이 더 쉽고 빠른 경향이 있으며, 실행 효율도 더 높고, 훨씬 더 신뢰할 수 있다” — Edsger Dijkstra
무엇이 잘못될 수 있을까요? 무엇이 잘못되었을까요? 우리는 어떤 질문을 더 하고 싶을까요? 전자입니다. 코드도 강철처럼 뜨거울 때 바꾸는 비용이 더 적게 들기 때문입니다. 프로덕션에서 해결하는 문제는 구현 단계에서 해결하는 문제보다, 그리고 설계 단계에서 해결하는 문제보다 훨씬 더 비쌉니다.
치명적인 문제를 발견하는 것 자체가 충분히 어렵기 때문에, 우리가 그것을 찾아냈다면 해결합니다. 잠재적인 memcpy 지연 시간 급증이나 지수 복잡도 알고리즘이 슬그머니 통과하도록 두지 않습니다.
“지나갈 수 없다!” — Gandalf
다시 말해, TigerBeetle은 “기술 부채 제로” 정책을 갖고 있습니다. 처음부터 올바르게 합니다. 이것이 중요한 이유는 두 번째 기회가 오지 않을 수도 있기 때문이며, 우리가 자부심을 가질 수 있는 좋은 일을 하는 것이 추진력을 만들기 때문입니다.
우리는 우리가 출하하는 것이 견고하다는 것을 압니다. 중요한 기능이 부족할 수는 있지만, 우리가 가진 것은 우리의 설계 목표를 충족합니다. 이것만이 우리가 이룬 진전이 실제로 진전임을 알면서 꾸준히 점진적으로 나아갈 수 있는 유일한 방법입니다.
“규칙은 자동차의 안전벨트처럼 작동한다. 처음에는 조금 불편할 수도 있지만, 얼마 지나지 않으면 사용하는 것이 제2의 천성이 되고, 사용하지 않는 것은 상상할 수 없게 된다.” — Gerard J. Holzmann
NASA의 Power of Ten — 안전 필수 코드를 개발하기 위한 규칙은 여러분의 코딩 방식을 영원히 바꿔놓을 것입니다. 확장해서 말하면:
명확성을 위해 오직 매우 단순하고 명시적인 제어 흐름만 사용하세요. 경계 지어져야 하는 모든 실행이 실제로 경계 지어지도록 하기 위해 재귀를 사용하지 마세요. 최소한의 훌륭한 추상화만 사용하되, 오직 그것이 도메인을 가장 잘 설명할 때만 사용하세요. 추상화는 결코 비용이 0이 아닙니다. 모든 추상화는 새는 추상화가 될 위험을 도입합니다.
모든 것에 한계를 두세요. 현실에서 우리가 기대하는 것이 바로 그것이기 때문입니다—모든 것에는 한계가 있습니다. 예를 들어, 모든 루프와 모든 큐는 무한 루프나 꼬리 지연 시간 급증을 방지하기 위해 고정된 상한을 가져야 합니다. 이는 위반을 나중이 아니라 더 빨리 탐지하도록 하는 “fail-fast” 원칙을 따릅니다. 루프가 종료될 수 없는 경우(예: 이벤트 루프)에는 이를 assert해야 합니다.
모든 것에 u32 같은 명시적 크기의 타입을 사용하고, 아키텍처별 usize는 피하세요.
assert는 프로그래머 오류를 탐지합니다. 예상되며 처리되어야 하는 운영 오류와 달리, assert 실패는 예상되지 않습니다. 손상된 코드를 처리하는 유일하게 올바른 방법은 크래시하는 것입니다. assert는 치명적인 정확성 버그를 라이브니스 버그로 낮춥니다. assert는 퍼징으로 버그를 발견하는 데 배가 효과를 냅니다.
모든 함수 인자와 반환값, 선행/후행 조건, 불변식을 assert하세요. 함수는 검사하지 않은 데이터를 맹목적으로 다뤄서는 안 됩니다. 함수의 목적은 프로그램이 올바를 확률을 높이는 것입니다. 함수 내부의 assert는 함수가 이 목적을 수행하는 방식의 일부입니다. 코드의 assert 밀도는 함수당 평균 최소 두 개의 assert여야 합니다.
assert를 짝지으세요. 강제하고 싶은 모든 속성에 대해, assert를 추가할 수 있는 서로 다른 코드 경로를 최소 두 개 찾으려고 하세요. 예를 들어, 데이터를 디스크에 쓰기 직전에 그 유효성을 assert하고, 디스크에서 읽은 직후에도 다시 assert하세요.
때로는 주석 대신 노골적으로 참인 assert를 더 강한 문서화 수단으로 사용할 수 있습니다. assert 조건이 중요하고 의외일 때 그렇습니다.
복합 assert는 분리하세요: assert(a and b);보다 assert(a); assert(b);를 선호하세요.
전자가 아니라 후자가 읽기 더 단순하고, 조건이 실패할 때 더 정확한 정보를 제공합니다.
함의를 assert할 때는 단일 행 if를 사용하세요: if (a) assert(b).
정상성 점검으로서 컴파일 타임 상수들의 관계를 assert하고, 또한 미묘한 불변식 또는 타입 크기를 문서화하고 강제하세요. 컴파일 타임 assert는 프로그램이 실행되기 전 에 프로그램 설계의 무결성을 점검할 수 있기 때문에 매우 강력합니다.
assert의 황금률은 기대하는 양의 공간 을 assert하는 동시에 기대하지 않는 음의 공간 도 assert하는 것입니다. 데이터가 이 공간들 사이의 유효/무효 경계를 가로질러 움직이는 지점에서 흥미로운 버그가 자주 발견되기 때문입니다. 이것이 또한 테스트가 철저해야 하는 이유이기도 합니다. 유효한 데이터만이 아니라 무효한 데이터도, 그리고 유효한 데이터가 무효해지는 과정도 테스트해야 합니다.
assert는 안전망이지, 인간의 이해를 대체하는 수단이 아닙니다. 시뮬레이션 테스트에서는 퍼저를 신뢰하고 싶은 유혹이 있습니다. 하지만 퍼저는 버그의 존재만 증명할 수 있을 뿐, 그 부재는 증명할 수 없습니다. 따라서:
모든 메모리는 시작 시점에 정적으로 할당되어야 합니다. 초기화 이후에는 어떤 메모리도 동적으로 할당해서는 안 되며(또는 해제 후 재할당해서도 안 됩니다). 이렇게 하면 성능에 중대한 영향을 줄 수 있는 예측 불가능한 동작을 피할 수 있고, use-after-free도 피할 수 있습니다. 한 단계 더 나아가, 우리의 경험상 이것은 설계의 일부로서 가능한 모든 메모리 사용 패턴을 미리 고려하지 않는 설계보다 더 효율적이고, 더 단순하며, 더 높은 성능을 내고, 유지보수와 추론이 더 쉬운 설계로 이어집니다.
변수를 가능한 가장 작은 스코프에 선언하고, 스코프 내 변수 수를 최소화하여 변수가 오용될 확률을 줄이세요.
함수가 화면 하나에 들어오는 것과, 길이를 보려면 스크롤해야 하는 것 사이에는 뚜렷한 불연속이 있습니다. 이 물리적 이유로 우리는 함수당 70줄의 엄격한 상한을 강제합니다. 예술은 제약에서 탄생합니다. 70줄짜리 덩어리로 코드의 벽을 자르는 방법은 많지만, 제대로 느껴지는 분할은 소수뿐입니다. 몇 가지 경험칙:
if는 위로 밀고 for는 아래로 내리세요”.첫날부터 컴파일러의 가장 엄격한 설정에서 나오는 모든 컴파일러 경고를 소중히 여기세요.
프로그램이 외부 엔터티와 상호작용해야 할 때는 언제나 외부 이벤트에 반응하여 직접 일을 하지 마세요. 대신 프로그램은 자기 자신의 속도로 실행되어야 합니다. 이는 프로그램의 제어 흐름을 여러분이 통제하게 하므로 더 안전해질 뿐 아니라, 같은 이유로 성능도 향상됩니다(모든 이벤트마다 컨텍스트 스위칭하는 대신 배치 처리할 수 있기 때문입니다). 또한 일정 시간당 수행되는 작업의 경계를 유지하기도 더 쉬워집니다.
이 규칙들 외에도:
여러 불리언을 평가하는 복합 조건은 모든 경우가 처리되었는지 독자가 검증하기 어렵게 만듭니다. 중첩된 if/else 분기를 사용해 복합 조건을 단순 조건으로 분리하세요. 복잡한 else if 체인은 else { if { } } 트리로 분리하세요. 이렇게 하면 분기와 경우가 명확해집니다. 다시 한 번, 단일 if에 대응하는 else 분기도 필요한 것은 아닌지 생각해 보세요. 양의 공간과 음의 공간이 처리되거나 assert되도록 하기 위해서입니다.
부정은 쉽지 않습니다! 불변식은 긍정형으로 표현하세요. 길이와 인덱스를 다룰 때는 다음 형식이 올바르게 작성하기도 쉽고(이해하기도 쉽습니다):
if (index < length) {
// 불변식이 성립한다.
} else {
// 불변식이 성립하지 않는다.
}
다음 형식은 더 어렵고, 예를 들어 루프 조건에서 index를 보통 length와 비교하는 방식의 결에도 어긋납니다:
if (index >= length) {
// 불변식이 성립한다는 것이 참이 아니다.
}
모든 오류는 처리되어야 합니다. 분산 데이터 집약 시스템의 프로덕션 장애에 대한 분석에 따르면, 치명적 장애의 대다수는 오류 처리 코드를 단순히 테스트하는 것만으로도 예방할 수 있었습니다.
“구체적으로, 우리는 거의 모든 (92%) 치명적 시스템 장애가 소프트웨어에서 명시적으로 신호된 비치명적 오류를 잘못 처리한 결과임을 발견했다.”
항상 동기를 설명하고, 항상 왜인지 말하세요. 왜인지 말하는 것을 절대 잊지 마세요. 결정의 근거를 설명하면 상대의 이해를 높이고, 그들이 더 잘 따르거나 수용하게 만들 뿐 아니라, 그 결정과 그 중요성을 평가할 수 있는 기준도 함께 공유하게 되기 때문입니다.
기본값에 의존하지 말고, 라이브러리 함수 호출 위치에서 옵션을 명시적으로 전달하세요. 예를 들어 @prefetch(a, .{ .cache = .data, .rw = .read, .locality = 3 });를 @prefetch(a, .{});보다 쓰세요. 이는 가독성을 높이며, 무엇보다도 라이브러리가 언젠가 기본값을 바꿀 경우 잠재적으로 치명적인 잠복 버그를 피하게 해줍니다.
“대충 계산해 보는 성능 스케치의 부재가 모든 악의 근원이다.” — Rivacindela Hudsoni
처음부터, 시작할 때부터 성능을 생각하세요. 성능을 해결해 엄청난 1000배의 이득을 얻기에 가장 좋은 시점은 설계 단계이며, 바로 그때는 측정도 프로파일링도 할 수 없습니다. 또한 보통 구현과 프로파일링 이후에 시스템을 고치기는 더 어렵고, 얻는 이득도 더 적습니다. 그러므로 기계적 공감 능력이 있어야 합니다. 목수처럼, 결을 따라 작업하세요.
네 가지 자원(네트워크, 디스크, 메모리, CPU)과 그 두 가지 주요 특성(대역폭, 지연 시간)에 대해 대충 계산한 스케치를 하세요. 스케치는 저렴합니다. “대략적으로 맞는” 방향을 잡고 전역 최대치의 90% 이내에 착지하기 위해 스케치를 사용하세요.
사용 빈도를 보정한 뒤, 가장 느린 자원부터(네트워크, 디스크, 메모리, CPU 순서로) 최적화하세요. 더 빠른 자원은 훨씬 더 많이 사용될 수 있기 때문입니다. 예를 들어, 메모리 캐시 미스가 훨씬 더 자주 발생한다면 디스크 fsync만큼 비쌀 수 있습니다.
제어 평면과 데이터 평면을 구분하세요. 배치 처리를 통해 제어 평면과 데이터 평면을 명확히 구분하면 성능을 잃지 않으면서도 높은 수준의 assert 안전성을 확보할 수 있습니다. 예시는 우리의 2021년 7월 Zig SHOWTIME 발표를 보세요.
배치 처리로 네트워크, 디스크, 메모리, CPU 비용을 상각하세요.
CPU를 100m를 뛰는 단거리 선수처럼 대하세요. 예측 가능하게 하세요. CPU가 지그재그로 움직이거나 차선을 바꾸도록 강요하지 마세요. CPU에게 충분히 큰 작업 덩어리를 주세요. 이것은 다시 배치 처리로 돌아옵니다.
명시적으로 하세요. 컴파일러가 여러분을 위해 올바른 일을 해주리라는 의존성을 최소화하세요.
특히 self 없이 원시 인자를 받는 독립 함수로 핫 루프를 분리하세요(예시 참조).
이렇게 하면 컴파일러가 struct의 필드를 레지스터에 캐시할 수 있음을 증명할 필요가 없고, 사람 독자도 중복 계산을 더 쉽게 발견할 수 있습니다.
“컴퓨터 과학에는 어려운 일이 두 가지뿐이다: 캐시 무효화, 이름 짓기, 그리고 off-by-one 오류.” — Phil Karlton
명사와 동사를 정확히 맞추세요. 훌륭한 이름은 훌륭한 코드의 정수입니다. 그것은 어떤 것이 무엇인지, 또는 무엇을 하는지를 포착하고, 날카롭고 직관적인 정신 모델을 제공합니다. 그것은 여러분이 도메인을 이해하고 있음을 보여줍니다. 완벽한 이름을 찾기 위해, 함께 잘 작동하는 명사와 동사를 찾기 위해 시간을 들이세요. 그래서 전체가 부분의 합보다 커지도록 하세요.
함수, 변수, 파일 이름에는 snake_case를 사용하세요. 언더스코어는 프로그래머에게 공간에 가장 가까운 것이며, 단어를 분리하고 설명적인 이름을 장려하는 데 도움이 됩니다. 규칙을 단순하고 일관되게 유지하기 위해 “struct” 파일에 Zig의 CamelCase.zig 스타일을 사용하지 않습니다.
변수 이름을 축약하지 마세요. 단, 그 변수가 정렬 함수나 행렬 계산의 인자로 쓰이는 원시 정수 타입인 경우는 예외입니다. 스크립트에서는 긴 형식의 인자를 사용하세요: -f가 아니라 --force입니다. 한 글자 플래그는 대화형 사용을 위한 것입니다.
약어에는 올바른 대문자 규칙을 사용하세요(VSRState, VsrState가 아님).
나머지는 Zig 스타일 가이드를 따르세요.
변수 이름에 단위나 한정자를 추가하고, 단위나 한정자는 중요도가 높은 것부터 낮은 것 순으로 뒤에 배치하세요. 그러면 변수는 가장 중요한 단어로 시작하고 가장 덜 중요한 단어로 끝나게 됩니다. 예를 들어 max_latency_ms보다 latency_ms_max입니다. 그러면 나중에 latency_ms_min이 추가될 때도 보기 좋게 정렬되고, 지연 시간과 관련된 변수들이 함께 묶입니다.
이름에 의미를 불어넣으세요. 예를 들어 allocator: Allocator는 괜찮지만 다소 밋밋한 이름이고,
gpa: Allocator와 arena: Allocator는 탁월합니다. 이 이름들은 deinit를 명시적으로 호출해야 하는지를 독자에게 알려줍니다.
관련된 이름을 고를 때는 같은 수의 문자를 가진 이름을 찾기 위해 노력하세요. 그래야 관련 변수들이 소스에서 모두 정렬됩니다. 예를 들어 memcpy 함수의 인자라면, src와 dest보다 source와 target이 더 좋습니다. source_offset과 target_offset 같은 관련 변수들도 계산과 슬라이스에서 모두 정렬되는 2차 효과가 있기 때문입니다. 이렇게 하면 코드는 대칭적이 되고, 눈으로 파싱하기 쉽고 독자가 확인하기 쉬운 깔끔한 블록이 생깁니다.
단일 함수가 헬퍼 함수나 콜백을 호출할 때는, 호출 이력을 보여주기 위해 헬퍼 함수 이름 앞에 호출하는 함수 이름을 접두어로 붙이세요. 예를 들어 read_sector()와 read_sector_callback()입니다.
콜백은 매개변수 목록의 마지막에 두세요. 이것은 제어 흐름을 반영합니다. 콜백도 호출되는 순서가 마지막이기 때문입니다.
가독성을 위해서는 순서 가 중요합니다(의미론에 영향을 주지 않더라도 그렇습니다). 파일은 처음 읽을 때 위에서 아래로 읽히므로, 중요한 것은 위쪽에 두세요. main 함수가 먼저 갑니다.
struct도 마찬가지로, 순서는 필드 다음 타입 다음 메서드입니다:
time: Time,
process_id: ProcessID,
const ProcessID = struct { cluster: u128, replica: u8 };
const Tracer = @This(); // 이 별칭이 타입 섹션을 마무리한다.
pub fn init(gpa: std.mem.Allocator, time: Time) !Tracer {
...
}
중첩 타입이 복잡하다면 최상위 struct로 빼세요.
동시에, 모든 것에 단 하나의 올바른 순서가 있는 것은 아닙니다. 확신이 서지 않으면, 빅엔디언 이름 짓기의 이점을 활용해 알파벳순 정렬을 고려하세요.
문맥에 따라 달라지는 여러 의미로 이름을 과적재하지 마세요. 예를 들어 TigerBeetle에는 pending transfers 라는 기능이 있고, pending transfer는 이후 posted 되거나 voided 될 수 있습니다. 처음에는 이것을 two-phase commit transfers 라고 불렀지만, 이것이 우리의 합의 프로토콜에서 사용하던 two-phase commit 용어와 겹쳐 혼란을 일으켰습니다.
이름이 코드 밖에서, 문서나 의사소통에서 어떻게 사용될지도 생각하세요. 예를 들어 명사는 형용사나 현재분사보다 더 좋은 설명자일 때가 많습니다. 명사는 다시 표현하지 않고도 서신이나 대화에서 직접 쓸 수 있기 때문입니다. replica.pipeline과 replica.preparing을 비교해 보세요. 전자는 문서나 대화에서 섹션 제목으로 직접 쓸 수 있지만, 후자는 설명이 필요합니다. 명사 이름은 파생 식별자에서도 더 명확하게 조합됩니다. 예: config.pipeline_max.
Zig는 options: struct 패턴을 통해 이름 있는 인자를 제공합니다. 인자가 헷갈릴 수 있을 때 이것을 사용하세요. u64 두 개를 받는 함수는 반드시 options struct를 사용해야 합니다. 인자가 null이 될 수 있다면, 호출 위치에서 null 리터럴의 의미가 분명해지도록 이름을 붙여야 합니다.
allocator나 tracer 같은 의존성은 고유한 타입을 가진 싱글턴이므로, 생성자를 통해 가장 일반적인 것에서 가장 구체적인 것으로 위치 기반으로 전달되어야 합니다.
독자에게 정보를 주고 기쁨을 주는 설명적인 커밋 메시지를 작성하세요. 여러분의 커밋 메시지는 읽히고 있기 때문입니다. pull request 설명은 git 저장소에 저장되지 않고 git blame에서도 보이지 않으므로, 커밋 메시지를 대체하지 않는다는 점에 유의하세요.
왜인지 말하는 것을 잊지 마세요. 코드만으로는 문서가 아닙니다. 왜 그렇게 코드를 썼는지 설명하기 위해 주석을 사용하세요. 여러분의 작업 과정을 보여주세요.
어떻게인지 말하는 것도 잊지 마세요. 예를 들어 테스트를 작성할 때는, 독자가 빠르게 맥락을 잡거나 섹션을 건너뛸 수 있도록 테스트의 목표와 방법론을 설명하는 설명을 맨 위에 쓰는 것을 생각해 보세요. 그러면 독자가 바로 깊이 파고들 필요가 없습니다.
주석은 문장입니다. 슬래시 뒤에는 공백을 두고, 대문자로 시작하며, 마침표를 찍거나, 뒤따르는 것과 관련된다면 콜론을 사용하세요. 주석은 코드에 대해 설명하는 잘 쓰인 산문이지, 여백의 낙서가 아닙니다. 줄 끝 뒤의 주석은 구 일 수 있으며, 그 경우 문장 부호는 없어도 됩니다.
변수를 중복시키거나 변수에 대한 별칭을 만들지 마세요. 이렇게 하면 상태가 어긋날 확률이 줄어듭니다.
함수 인자가 값으로 전달될 때 복사되기를 의도하지 않았고, 그 인자 타입이 16바이트를 초과한다면, 인자를 *const로 전달하세요. 이렇게 하면 호출자가 함수를 호출하기 전에 실수로 스택에 복사본을 만드는 버그를 잡을 수 있습니다.
초기화 중에 out pointer 를 전달하여 더 큰 struct를 제자리에서 구성하세요.
제자리 초기화는 중간 복사-이동 할당을 제거하면서 포인터 안정성과 이동 불가능 타입을 가정할 수 있게 해주며, 이는 바람직하지 않은 스택 증가로 이어질 수 있습니다.
제자리 초기화는 전염성이 있다는 점을 염두에 두세요 — 어떤 필드라도 제자리에서 초기화된다면, 전체 컨테이너 struct도 제자리에서 초기화되어야 합니다.
권장:
fn init(target: *LargeStruct) !void {
target.* = .{
// 제자리 초기화.
};
}
fn main() !void {
var target: LargeStruct = undefined;
try target.init();
}
비권장:
fn init() !LargeStruct {
return LargeStruct {
// 초기화된 객체를 이동한다.
}
}
fn main() !void {
var target = try LargeStruct.init();
}
스코프를 줄이세요. 작용 중인 변수 수를 최소화하고 잘못된 변수가 사용될 확률을 줄이기 위해서입니다.
변수를 사용되는 곳/시점 가까이에서 계산하거나 검사하세요. 필요해지기 전에 변수를 도입하지 마세요. 필요하지 않은 곳에 그대로 두지 마세요. 이렇게 하면 악명 높은 TOCTOU의 먼 친척인 POCPOU (place-of-check to place-of-use)의 확률을 줄일 수 있습니다. 대부분의 버그는 시간이나 공간의 간극 때문에 생기는 의미적 간극으로 귀결되며, 그 차원들에 따라 코드가 한곳에 모여 있지 않으면 검토하기가 더 어렵기 때문입니다.
함수 시그니처와 반환 타입을 더 단순하게 사용하여 호출 위치의 차원 수, 즉 호출 위치에서 처리해야 하는 분기 수를 줄이세요. 이런 차원성도 호출 체인을 따라 전염될 수 있기 때문입니다. 예를 들어 반환 타입으로는 void가 bool보다 낫고, bool이 u64보다 낫고, u64가 ?u64보다 낫고, ?u64가 !u64보다 낫습니다.
함수가 중단 없이 끝까지 실행되도록 하세요. 그래야 선행 조건 assert가 함수의 생애 동안 참으로 유지됩니다. 중단이 없다면 이 assert들은 유용한 문서화가 되지만, 그렇지 않다면 오해를 부를 수 있습니다.
buffer bleeds 를 경계하세요. 이것은 버퍼 오버플로의 반대인 버퍼 언더플로로, 버퍼가 완전히 사용되지 않고 패딩이 올바르게 0으로 채워지지 않는 경우입니다. 이는 민감한 정보를 누출할 수 있을 뿐 아니라, TigerBeetle에 필요한 결정론적 보장을 위반하게 만들 수도 있습니다.
누수를 더 쉽게 발견할 수 있도록, 리소스 할당 전과 대응하는 defer 문 뒤에 새 줄을 넣어 리소스의 할당과 해제를 그룹화하세요.
off-by-one 오류의 흔한 용의자는 index, count, size 사이의 무심한 상호작용입니다. 이들은 모두 원시 정수 타입이지만, 서로 구별되는 타입으로 보고 그 사이를 캐스팅하는 명확한 규칙을 가져야 합니다. index에서 count로 가려면 하나를 더해야 합니다. 인덱스는 0-based 이고 카운트는 1-based 이기 때문입니다. count에서 size로 가려면 단위를 곱해야 합니다. 다시 말해, 이것이 변수 이름에 단위와 한정자를 포함하는 것이 중요한 이유입니다.
나눗셈에 대한 의도를 드러내세요. 예를 들어, 반올림이 관련될 수 있는 흥미로운 시나리오들을 모두 고려했음을 독자에게 보여주기 위해 @divExact(), @divFloor() 또는 div_ceil()을 사용하세요.
zig fmt를 실행하세요.
2칸 공백보다, 멀리서 봐도 더 분명한 4칸 들여쓰기를 사용하세요.
좋은 타이포그래피적 “measure”를 위해 예외 없이 모든 줄 길이에 엄격한 상한을 두고, 최대 100열을 넘지 마세요. 그것을 충분히 활용하되, 절대 넘어가지 마세요. 가로 스크롤바에 가려지는 것은 아무것도 없어야 합니다. 에디터에 열 눈금자를 설정하여 도움을 받으세요. 함수 시그니처, 호출, 또는 데이터 구조를 줄바꿈하려면 후행 쉼표를 추가하고, 눈을 감고 zig fmt가 나머지를 하게 두세요.
함수 길이와 마찬가지로 숫자 100의 동기도 물리적입니다. 화면에 코드 두 벌을 나란히 띄우기에 딱 충분하기 때문입니다.
일관성과 “goto fail;” 버그에 대한 심층 방어를 위해, 단일 줄에 들어맞는 경우가 아니라면 if 문에 중괄호를 추가하세요.
TigerBeetle은 Zig 툴체인을 제외하고는 “의존성 제로” 정책을 갖고 있습니다. 일반적으로 의존성은 필연적으로 공급망 공격, 안전성과 성능 위험, 느린 설치 시간으로 이어집니다. 특히 기반 인프라에서는 어떤 의존성이든 그 비용이 스택의 나머지 부분 전체에 걸쳐 더욱 증폭됩니다.
마찬가지로 도구에도 비용이 있습니다. 작고 표준화된 도구상자는 각각 전용 설명서를 가진 전문 도구들의 배열보다 운영하기가 더 단순합니다. 우리의 주된 도구는 Zig입니다. 모든 것에 최고는 아닐 수 있지만, 대부분의 일에는 충분히 좋습니다. 우리는 로컬 개발 환경의 우발적 복잡성을 최소화하면서 새로운 문제를 빠르게 다룰 수 있도록 Zig 도구 체인에 투자합니다.
“작업에 맞는 올바른 도구는 종종 이미 사용하고 있는 도구다—새 도구를 추가하는 데는 많은 사람들이 생각하는 것보다 더 큰 비용이 든다” — John Carmack
예를 들어, 다음에 스크립트를 작성할 때는 scripts/*.sh 대신 scripts/*.zig를 쓰세요.
이렇게 하면 스크립트가 크로스플랫폼이고 이식 가능해질 뿐 아니라, 타입 안전성이 도입되고, 여러분의 스크립트 실행이 팀의 모든 사람에게 성공할 확률이 높아집니다. Bash/Shell/OS별 문제에 부딪히는 대신 말입니다.
도구용 언어로 Zig를 표준화하는 것은 팀이 성장하고, 따라서 개인 취향의 범위도 커질 때 차원성을 줄이기 위해 중요합니다. 이것은 단기적으로는 여러분에게 더 느릴 수 있지만, 장기적으로는 팀 전체의 속도를 높여줍니다.
하루를 마무리할 때, 계속 이것저것 시도해 보고, 즐기고, 기억하세요—TigerBeetle이라는 이름은 빠르기 때문만이 아니라, 작기 때문이기도 합니다!
자네는 설마 자네의 모든 모험과 탈출이 오직 자네 자신의 이익만을 위해, 그저 운으로 이루어졌다고 정말 생각하는 건 아니겠지? 자네는 아주 훌륭한 사람이네, Baggins 씨, 그리고 나는 자네를 아주 좋아하네; 하지만 결국 자네는 넓은 세상 속의 아주 작은 존재일 뿐이야!”
“정말 다행이네요!” Bilbo가 웃으며 말하고는 그에게 담배 단지를 건넸다.