1. 소개

Expr은 간단한 구문과 강력한 성능 특징으로 알려진 Go 언어를 위한 동적 구성 솔루션으로, 안전성, 속도 및 직관성에 중점을 둔 Expr 표현 엔진의 핵심은 액세스 제어, 데이터 필터링 및 리소스 관리와 같은 시나리오에 적합하다. Go에 적용되면 Expr은 애플리케이션이 동적 규칙을 처리하는 능력을 크게 향상시킨다. 다른 언어의 해석기나 스크립트 엔진과는 달리 Expr은 정적 유형 검사를 채택하고 실행을 위해 바이트 코드를 생성하여 성능과 보안을 보장한다.

2. Expr 설치하기

Go 언어의 패키지 관리 도구 go get을 사용하여 Expr 표현 엔진을 설치할 수 있다:

go get github.com/expr-lang/expr

이 명령은 Expr 라이브러리 파일을 다운로드하고 Go 프로젝트에 설치하여 Go 코드에서 Expr을 가져와 사용할 수 있게 한다.

3. 빠른 시작

3.1 기본 표현식 컴파일 및 실행

간단한 예제로 시작해보자: 간단한 표현식을 작성한 다음 컴파일하고 결과를 얻기 위해 실행하는 것이다.

package main

import (
	"fmt"
	"github.com/expr-lang/expr"
)

func main() {
	// 기본 덧셈 표현식 컴파일
	program, err := expr.Compile(`2 + 2`)
	if err != nil {
		panic(err)
	}

	// 환경을 전달하지 않고 컴파일된 표현식 실행
	output, err := expr.Run(program, nil)
	if err != nil {
		panic(err)
	}

	// 결과 출력
	fmt.Println(output)  // 결과: 4
}

이 예제에서는 표현식 2 + 2가 실행 가능한 바이트 코드로 컴파일되고, 그런 다음 실행하여 결과를 생성한다.

3.2 변수 표현식 사용하기

다음으로 변수를 포함하는 환경을 만들고, 이러한 변수를 사용하는 표현식을 작성하고 컴파일하여 실행하는 예제를 살펴보자.

package main

import (
	"fmt"
	"github.com/expr-lang/expr"
)

func main() {
	// 변수를 포함하는 환경 생성
	env := map[string]interface{}{
		"foo": 100,
		"bar": 200,
	}

	// 환경에서 변수를 사용하는 표현식 컴파일
	program, err := expr.Compile(`foo + bar`, expr.Env(env))
	if err != nil {
		panic(err)
	}

	// 표현식 실행
	output, err := expr.Run(program, env)
	if err != nil {
		panic(err)
	}

	// 결과 출력
	fmt.Println(output)  // 결과: 300
}

이 예제에서 환경 env에는 foobar 변수가 포함되어 있다. 표현식 foo + bar은 컴파일 시 환경에서 foobar의 유형을 추론하고, 실행 시에 이러한 변수의 값들을 사용하여 표현식 결과를 평가한다.

4. 자세한 Expr 구문

4.1 변수 및 리터럴

Expr 표현 엔진은 숫자, 문자열, 부울 값을 포함한 공통 데이터 유형 리터럴을 처리할 수 있다. 리터럴은 코드에 직접 작성된 데이터 값들을 나타낸다. 예를 들어 42, "hello", true 등이다.

숫자

Expr에서 정수와 부동 소수점 숫자를 직접 작성할 수 있다:

42      // 정수 42를 나타냄
3.14    // 부동 소수점 숫자 3.14를 나타냄

문자열

문자열 리터럴은 이중 인용부호 " 또는 백쿼트 ``로 둘러싸여 있다. 예:

"hello, world" // 이중 인용부호로 둘러싸인 문자열, 이스케이프 문자 지원
`hello, world` // 백쿼트로 둘러싸인 문자열, 이스케이프 문자 지원하지 않음

부울 값

truefalse 두 개의 부울 값만 있으며, 논리적인 참과 거짓을 나타낸다:

true   // 부울 참 값
false  // 부울 거짓 값

변수

Expr은 환경에 변수를 정의하고, 그런 다음 표현식에서 이러한 변수를 참조하는 것도 허용한다. 예:

env := map[string]interface{}{
    "age": 25,
    "name": "Alice",
}

그런 다음 표현식에서 agename을 참조할 수 있다:

age > 18  // age가 18보다 큰지 확인
name == "Alice"  // 이름이 "Alice"와 같은지 확인

4.2 연산자

Expr 표현 엔진은 산술 연산자, 논리 연산자, 비교 연산자, 집합 연산자 등 다양한 연산자를 지원한다.

산술 및 논리 연산자

산술 연산자에는 덧셈(+), 뺄셈(-), 곱셈(*), 나눗셈(/), 나머지(%)가 포함됩니다. 논리 연산자에는 논리 AND(&&), 논리 OR(||), 그리고 논리 NOT(!)가 포함됩니다. 예를 들어:

2 + 2 // 결과는 4
7 % 3 // 결과는 1
!true // 결과는 false
age >= 18 && name == "Alice" // 나이가 18보다 작지 않고 이름이 "Alice"인지 확인합니다

비교 연산자

비교 연산자에는 같음(==), 같지 않음(!=), 작음(<), 작거나 같음(<=), 큼(>), 그리고 크거나 같음(>=)이 포함되어 두 값 비교에 사용됩니다:

age == 25 // 나이가 25인지 확인합니다
age != 18 // 나이가 18이 아닌지 확인합니다
age > 20  // 나이가 20보다 큰지 확인합니다

집합 연산자

Expr은 집합과 관련된 작업을 위한 몇 가지 연산자를 제공하는데, 예를 들어 in을 사용하여 원소가 집합에 있는지 확인할 수 있습니다. 집합은 배열, 슬라이스 또는 맵일 수 있습니다:

"user" in ["user", "admin"]  // 배열에 "user"가 있으므로 true
3 in {1: true, 2: false}     // 맵의 키에 3이 없으므로 false

또한 all, any, one, none과 같은 고급 집합 연산 함수도 있지만, 익명 함수(lambda)의 사용이 필요합니다:

all(tweets, {.Len <= 240})  // 모든 트윗의 Len 필드가 240을 넘지 않는지 확인합니다
any(tweets, {.Len > 200})   // 트윗 중 Len 필드가 200을 넘는 것이 있는지 확인합니다

멤버 연산자

Expr 표현 언어에서 멤버 연산자를 사용하면 Go 언어의 struct의 속성에 접근할 수 있습니다. 이 기능은 Expr이 복합 데이터 구조를 직접 조작할 수 있도록 하여 매우 유연하고 실용적입니다.

멤버 연산자를 사용하는 것은 매우 간단합니다. . 연산자 다음에 속성 이름을 사용하면 됩니다. 예를 들어, 다음과 같은 struct가 있다고 가정해봅시다:

type User struct {
    Name string
    Age  int
}

다음과 같이 User 구조체의 Name 속성에 액세스하는 표현을 작성할 수 있습니다:

env := map[string]interface{}{
    "user": User{Name: "Alice", Age: 25},
}

code := `user.Name`

program, err := expr.Compile(code, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
    panic(err)
}

fmt.Println(output) // 출력: Alice

nil 값 처리

속성에 접근할 때 객체가 nil인 상황을 마주칠 수 있습니다. Expr은 안전한 속성 액세스를 제공하여 구조체나 중첩된 속성이 nil이더라도 런타임 패닉 에러를 발생시키지 않습니다.

?. 연산자를 사용하여 속성을 참조합니다. 객체가 nil이면 에러를 발생시키는 대신 nil을 반환합니다.

author.User?.Name

동일한 표현식

author.User != nil ? author.User.Name : nil

?? 연산자는 주로 기본 값 반환을 위해 사용됩니다:

author.User?.Name ?? "Anonymous"

동일한 표현식

author.User != nil ? author.User.Name : "Anonymous"

파이프 연산자

Expr의 파이프 연산자(|)는 한 표현식의 결과를 다른 표현식의 매개변수로 전달하는 데 사용됩니다. 이는 Unix 셸의 파이프 연산과 유사하며 여러 기능 모듈을 연결하여 처리 파이프라인을 형성하는 것을 가능하게 합니다. Expr에서 이를 사용하면 더 명확하고 간결한 표현식을 만들 수 있습니다.

예를 들어, 사용자의 이름을 가져오는 함수와 환영 메시지를 위한 템플릿이 있다고 가정해봅시다:

env := map[string]interface{}{
    "user":      User{Name: "Bob", Age: 30},
    "get_name":  func(u User) string { return u.Name },
    "greet_msg": "안녕, %s!",
}

code := `get_name(user) | sprintf(greet_msg)`

program, err := expr.Compile(code, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
    panic(err)
}

fmt.Println(output) // 출력: 안녕, Bob!

이 예제에서는 먼저 get_name(user)를 통해 사용자의 이름을 가져온 다음, 파이프 연산자 |를 사용하여 이름을 sprintf 함수에 전달하여 최종 환영 메시지를 생성합니다.

파이프 연산자를 사용하면 코드를 모듈화하고 코드의 재사용성을 높이며 표현식을 더 가독성 있게 만들 수 있습니다.

4.3 함수

Expr은 내장 함수와 사용자 정의 함수를 지원하여 표현식을 더 강력하고 유연하게 만들어줍니다.

내장 함수 사용하기

len, all, none, any 등과 같은 내장 함수는 표현식에서 직접 사용할 수 있습니다.

// 내장 함수 사용 예시
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
    panic(err)
}

// 참고: 여기서 env에는 users 변수가 포함되어야 하며, 각 사용자는 Age 속성을 가져야 합니다
output, err := expr.Run(program, env)
fmt.Print(output) // env 내의 모든 사용자가 18세 이상이면 true를 반환합니다

사용자 정의 함수 정의 및 사용하는 방법

Expr에서는 환경 매핑에 함수 정의를 전달하여 사용자 정의 함수를 만들 수 있습니다.

// 사용자 정의 함수 예시
env := map[string]interface{}{
    "greet": func(name string) string {
        return fmt.Sprintf("안녕, %s!", name)
    },
}

program, err := expr.Compile(`greet("World")`, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
fmt.Print(output) // 결과: 안녕, World!

Expr에서 함수를 사용할 때, 변수, 연산자 및 함수를 결합하여 코드를 모듈화하고 복잡한 로직을 표현식에 통합할 수 있습니다. Expr 환경을 구축하고 표현식을 실행할 때 항상 타입 안전성을 유지하는 것을 기억해야 합니다.

5. 내장 함수 문서화

Expr 표현식 엔진은 다양한 복잡한 시나리오를 다루기 위한 풍부한 내장 함수 세트를 개발자에게 제공합니다. 아래에서 이러한 내장 함수들과 그 사용법에 대해 상세히 설명하겠습니다.

all

all 함수는 컬렉션 내의 모든 요소가 주어진 조건을 만족하는지 확인하는 데 사용됩니다. 두 개의 매개변수, 즉 컬렉션과 조건 표현식을 가져옵니다.

// 모든 트윗이 내용 길이가 240보다 작은지 확인
code := `all(tweets, len(.Content) < 240)`

any

all과 유사하게, any 함수는 컬렉션 내의 어떤 요소라도 조건을 만족하는지 확인하는 데 사용됩니다.

// 어떤 트윗이 내용 길이가 240보다 큰지 확인
code := `any(tweets, len(.Content) > 240)`

none

none 함수는 컬렉션 내의 어떤 요소도 조건을 만족하지 않는지 확인하는 데 사용됩니다.

// 중복되는 트윗이 없는지 확인
code := `none(tweets, .IsRepeated)`

one

one 함수는 컬렉션 내의 요소 중 하나만 조건을 만족하는지 확인하는 데 사용됩니다.

// 오직 하나의 트윗이 특정 키워드를 포함하는지 확인
code := `one(tweets, contains(.Content, "keyword"))`

filter

filter 함수는 주어진 조건을 만족하는 컬렉션 요소만 필터링하는 데 사용됩니다.

// 모든 우선순위로 표시된 트윗을 필터링
code := `filter(tweets, .IsPriority)`

map

map 함수는 컬렉션 내의 요소를 지정된 방법에 따라 변환하는 데 사용됩니다.

// 모든 트윗의 게시 시간을 형식화
code := `map(tweets, {.PublishTime: Format(.Date)})`

len

len 함수는 컬렉션 또는 문자열의 길이를 반환하는 데 사용됩니다.

// 사용자 이름의 길이 가져오기
code := `len(username)`

contains

contains 함수는 문자열이 특정 부분 문자열을 포함하거나 컬렉션이 특정 요소를 포함하는지 여부를 확인하는 데 사용됩니다.

// 사용자 이름에 불법 문자가 포함되어 있는지 확인
code := `contains(username, "불법 문자")`

위에서 언급한 것은 Expr 표현 엔진이 제공하는 내장 함수 중 일부에 불과합니다. 이러한 강력한 함수들로 데이터와 로직을 보다 유연하고 효율적으로 처리할 수 있습니다. 더 자세한 함수 목록 및 사용법은 공식 Expr 문서를 참고해주세요.